@sentry/junior 0.8.0 → 0.9.1

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,839 @@
1
+ import {
2
+ getPluginRuntimeDependencies,
3
+ getPluginRuntimePostinstall
4
+ } from "./chunk-ZBWWHP6Q.js";
5
+ import {
6
+ withSpan
7
+ } from "./chunk-ZW4OVKF5.js";
8
+
9
+ // src/chat/config.ts
10
+ var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
11
+ var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
12
+ var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
13
+ var TURN_TIMEOUT_BUFFER_SECONDS = 20;
14
+ function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
15
+ const value = Number.parseInt(rawValue ?? "", 10);
16
+ if (Number.isNaN(value)) {
17
+ return Math.max(
18
+ MIN_AGENT_TURN_TIMEOUT_MS,
19
+ Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs)
20
+ );
21
+ }
22
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
23
+ }
24
+ function resolveQueueCallbackMaxDurationSeconds(env) {
25
+ const value = Number.parseInt(
26
+ env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "",
27
+ 10
28
+ );
29
+ if (Number.isNaN(value) || value <= 0) {
30
+ return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
31
+ }
32
+ return value;
33
+ }
34
+ function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
35
+ const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
36
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
37
+ }
38
+ function toOptionalTrimmed(value) {
39
+ if (!value) {
40
+ return void 0;
41
+ }
42
+ const trimmed = value.trim();
43
+ return trimmed.length > 0 ? trimmed : void 0;
44
+ }
45
+ function readBotConfig(env) {
46
+ const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds(env);
47
+ const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(
48
+ queueCallbackMaxDurationSeconds
49
+ );
50
+ return {
51
+ userName: env.JUNIOR_BOT_NAME ?? "junior",
52
+ modelId: env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
53
+ fastModelId: env.AI_FAST_MODEL ?? env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
54
+ turnTimeoutMs: parseAgentTurnTimeoutMs(
55
+ env.AGENT_TURN_TIMEOUT_MS,
56
+ maxTurnTimeoutMs
57
+ )
58
+ };
59
+ }
60
+ function readChatConfig(env = process.env) {
61
+ return {
62
+ bot: readBotConfig(env),
63
+ queue: {
64
+ callbackMaxDurationSeconds: resolveQueueCallbackMaxDurationSeconds(env)
65
+ },
66
+ slack: {
67
+ botToken: toOptionalTrimmed(env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(env.SLACK_BOT_USER_TOKEN),
68
+ signingSecret: toOptionalTrimmed(env.SLACK_SIGNING_SECRET),
69
+ clientId: toOptionalTrimmed(env.SLACK_CLIENT_ID),
70
+ clientSecret: toOptionalTrimmed(env.SLACK_CLIENT_SECRET)
71
+ },
72
+ state: {
73
+ adapter: env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory" ? "memory" : "redis",
74
+ redisUrl: toOptionalTrimmed(env.REDIS_URL)
75
+ }
76
+ };
77
+ }
78
+ var chatConfig = readChatConfig(process.env);
79
+ function getChatConfig() {
80
+ return chatConfig;
81
+ }
82
+ var botConfig = chatConfig.bot;
83
+ function getSlackBotToken() {
84
+ return chatConfig.slack.botToken;
85
+ }
86
+ function getSlackSigningSecret() {
87
+ return chatConfig.slack.signingSecret;
88
+ }
89
+ function getSlackClientId() {
90
+ return chatConfig.slack.clientId;
91
+ }
92
+ function getSlackClientSecret() {
93
+ return chatConfig.slack.clientSecret;
94
+ }
95
+ function getRuntimeMetadata() {
96
+ return {
97
+ version: toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA)
98
+ };
99
+ }
100
+
101
+ // src/chat/state/adapter.ts
102
+ import { createMemoryState } from "@chat-adapter/state-memory";
103
+ import { createRedisState } from "@chat-adapter/state-redis";
104
+ var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
105
+ var stateAdapter;
106
+ var redisStateAdapter;
107
+ function createQueuedStateAdapter(base) {
108
+ const acquireLock = async (threadId, ttlMs) => {
109
+ const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
110
+ return await base.acquireLock(threadId, effectiveTtlMs);
111
+ };
112
+ return {
113
+ appendToList: (key, value, options) => base.appendToList(key, value, options),
114
+ connect: () => base.connect(),
115
+ disconnect: () => base.disconnect(),
116
+ subscribe: (threadId) => base.subscribe(threadId),
117
+ unsubscribe: (threadId) => base.unsubscribe(threadId),
118
+ isSubscribed: (threadId) => base.isSubscribed(threadId),
119
+ acquireLock,
120
+ releaseLock: (lock) => base.releaseLock(lock),
121
+ extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
122
+ forceReleaseLock: (threadId) => base.forceReleaseLock(threadId),
123
+ get: (key) => base.get(key),
124
+ getList: (key) => base.getList(key),
125
+ set: (key, value, ttlMs) => base.set(key, value, ttlMs),
126
+ setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
127
+ delete: (key) => base.delete(key)
128
+ };
129
+ }
130
+ function createStateAdapter() {
131
+ const config = getChatConfig();
132
+ if (config.state.adapter === "memory") {
133
+ redisStateAdapter = void 0;
134
+ return createQueuedStateAdapter(createMemoryState());
135
+ }
136
+ if (!config.state.redisUrl) {
137
+ throw new Error("REDIS_URL is required for durable Slack thread state");
138
+ }
139
+ const redisState = createRedisState({
140
+ url: config.state.redisUrl
141
+ });
142
+ redisStateAdapter = redisState;
143
+ return createQueuedStateAdapter(redisState);
144
+ }
145
+ function getOptionalRedisStateAdapter() {
146
+ getStateAdapter();
147
+ return redisStateAdapter;
148
+ }
149
+ async function getConnectedStateContext() {
150
+ const adapter = getStateAdapter();
151
+ await adapter.connect();
152
+ return {
153
+ redisStateAdapter: getOptionalRedisStateAdapter(),
154
+ stateAdapter: adapter
155
+ };
156
+ }
157
+ function getStateAdapter() {
158
+ if (!stateAdapter) {
159
+ stateAdapter = createStateAdapter();
160
+ }
161
+ return stateAdapter;
162
+ }
163
+ async function disconnectStateAdapter() {
164
+ if (!stateAdapter) {
165
+ return;
166
+ }
167
+ try {
168
+ await stateAdapter.disconnect();
169
+ } finally {
170
+ stateAdapter = void 0;
171
+ redisStateAdapter = void 0;
172
+ }
173
+ }
174
+
175
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
176
+ import { createHash } from "crypto";
177
+ import { Sandbox } from "@vercel/sandbox";
178
+
179
+ // src/chat/sandbox/noninteractive-command.ts
180
+ var NON_INTERACTIVE_ENV = {
181
+ CI: "1",
182
+ TERM: "dumb",
183
+ NO_COLOR: "1",
184
+ PAGER: "cat",
185
+ GIT_PAGER: "cat",
186
+ GH_PROMPT_DISABLED: "1",
187
+ GH_NO_UPDATE_NOTIFIER: "1",
188
+ GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
189
+ GH_SPINNER_DISABLED: "1",
190
+ GIT_TERMINAL_PROMPT: "0",
191
+ GCM_INTERACTIVE: "never",
192
+ DEBIAN_FRONTEND: "noninteractive"
193
+ };
194
+ function shellQuote(value) {
195
+ return `'${value.replace(/'/g, "'\\''")}'`;
196
+ }
197
+ function buildEnvExports(options) {
198
+ const lines = [];
199
+ if (options.pathPrefix) {
200
+ lines.push(`export PATH="${options.pathPrefix}"`);
201
+ }
202
+ for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) {
203
+ lines.push(`export ${key}=${shellQuote(value)}`);
204
+ }
205
+ for (const [key, value] of Object.entries(options.env ?? {})) {
206
+ lines.push(`export ${key}=${shellQuote(value)}`);
207
+ }
208
+ return lines;
209
+ }
210
+ function toCommandScript(input) {
211
+ return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(
212
+ " "
213
+ );
214
+ }
215
+ function buildNonInteractiveShellScript(script, options = {}) {
216
+ return [...buildEnvExports(options), "exec </dev/null", script].join(" && ");
217
+ }
218
+ function buildNonInteractiveCommand(input) {
219
+ return {
220
+ cmd: "bash",
221
+ args: [
222
+ input.login ? "-lc" : "-c",
223
+ buildNonInteractiveShellScript(toCommandScript(input), {
224
+ env: input.env,
225
+ pathPrefix: input.pathPrefix
226
+ })
227
+ ]
228
+ };
229
+ }
230
+ async function runNonInteractiveCommand(runner, input) {
231
+ return await runner.runCommand({
232
+ ...buildNonInteractiveCommand(input),
233
+ ...input.cwd ? { cwd: input.cwd } : {},
234
+ ...input.sudo !== void 0 ? { sudo: input.sudo } : {}
235
+ });
236
+ }
237
+
238
+ // src/chat/sandbox/credentials.ts
239
+ function toOptionalTrimmed2(value) {
240
+ if (!value) {
241
+ return void 0;
242
+ }
243
+ const trimmed = value.trim();
244
+ return trimmed.length > 0 ? trimmed : void 0;
245
+ }
246
+ function getVercelSandboxCredentials() {
247
+ const token = toOptionalTrimmed2(process.env.VERCEL_TOKEN);
248
+ const teamId = toOptionalTrimmed2(process.env.VERCEL_TEAM_ID);
249
+ const projectId = toOptionalTrimmed2(process.env.VERCEL_PROJECT_ID);
250
+ if (token && teamId && projectId) {
251
+ return { token, teamId, projectId };
252
+ }
253
+ if (toOptionalTrimmed2(process.env.VERCEL_OIDC_TOKEN)) {
254
+ return void 0;
255
+ }
256
+ if (token || teamId || projectId) {
257
+ throw new Error(
258
+ "Missing Vercel Sandbox credentials: set VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID together, or provide VERCEL_OIDC_TOKEN."
259
+ );
260
+ }
261
+ return void 0;
262
+ }
263
+
264
+ // src/chat/sandbox/paths.ts
265
+ function normalizeWorkspaceRoot(input) {
266
+ const candidate = (input ?? "").trim();
267
+ if (!candidate) {
268
+ return "/vercel/sandbox";
269
+ }
270
+ const normalized = candidate.replace(/\/+$/, "");
271
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
272
+ }
273
+ var SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(process.env.VERCEL_SANDBOX_WORKSPACE_DIR);
274
+ var SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
275
+ function sandboxSkillDir(skillName) {
276
+ return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
277
+ }
278
+
279
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
280
+ var SNAPSHOT_CACHE_PREFIX = "junior:sandbox_snapshot_profile";
281
+ var SNAPSHOT_LOCK_PREFIX = "junior:sandbox_snapshot_lock";
282
+ var SNAPSHOT_PROFILE_VERSION = 1;
283
+ var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
284
+ var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
285
+ var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
286
+ var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
287
+ function sleep(ms) {
288
+ return new Promise((resolve) => {
289
+ setTimeout(resolve, ms);
290
+ });
291
+ }
292
+ function profileCacheKey(profileHash) {
293
+ return `${SNAPSHOT_CACHE_PREFIX}:${profileHash}`;
294
+ }
295
+ function profileLockKey(profileHash) {
296
+ return `${SNAPSHOT_LOCK_PREFIX}:${profileHash}`;
297
+ }
298
+ function isExactNpmVersion(version) {
299
+ return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
300
+ }
301
+ function hasFloatingSelector(dep) {
302
+ return dep.type === "npm" && !isExactNpmVersion(dep.version);
303
+ }
304
+ function parseFloatingDepMaxAgeMs() {
305
+ const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
306
+ if (!raw?.trim()) {
307
+ return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
308
+ }
309
+ const parsed = Number.parseInt(raw, 10);
310
+ if (!Number.isFinite(parsed) || parsed < 0) {
311
+ return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
312
+ }
313
+ return parsed;
314
+ }
315
+ function buildDependencyProfile(runtime) {
316
+ const dependencies = getPluginRuntimeDependencies();
317
+ const postinstall = getPluginRuntimePostinstall();
318
+ if (dependencies.length === 0 && postinstall.length === 0) {
319
+ return null;
320
+ }
321
+ const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
322
+ const hasFloatingVersions = dependencies.some((dep) => hasFloatingSelector(dep)) || postinstall.length > 0;
323
+ const hashInput = JSON.stringify({
324
+ version: SNAPSHOT_PROFILE_VERSION,
325
+ runtime,
326
+ rebuildEpoch,
327
+ dependencies,
328
+ postinstall
329
+ });
330
+ const profileHash = createHash("sha256").update(hashInput).digest("hex");
331
+ return {
332
+ profileHash,
333
+ dependencyCount: dependencies.length,
334
+ hasFloatingVersions,
335
+ dependencies,
336
+ postinstall
337
+ };
338
+ }
339
+ function getRuntimeDependencyProfileHash(runtime) {
340
+ return buildDependencyProfile(runtime)?.profileHash;
341
+ }
342
+ function shouldRebuildCachedSnapshot(profile, cached) {
343
+ if (!profile.hasFloatingVersions) {
344
+ return false;
345
+ }
346
+ const maxAgeMs = parseFloatingDepMaxAgeMs();
347
+ if (maxAgeMs === 0) {
348
+ return true;
349
+ }
350
+ return Date.now() - cached.createdAtMs > maxAgeMs;
351
+ }
352
+ async function getCachedSnapshot(profileHash) {
353
+ try {
354
+ const state = getStateAdapter();
355
+ await state.connect();
356
+ const raw = await state.get(profileCacheKey(profileHash));
357
+ if (typeof raw !== "string") {
358
+ return null;
359
+ }
360
+ const parsed = JSON.parse(raw);
361
+ 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") {
362
+ return null;
363
+ }
364
+ return parsed;
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+ async function setCachedSnapshot(entry) {
370
+ const state = getStateAdapter();
371
+ await state.connect();
372
+ await state.set(
373
+ profileCacheKey(entry.profileHash),
374
+ JSON.stringify(entry),
375
+ SNAPSHOT_CACHE_TTL_MS
376
+ );
377
+ }
378
+ async function withSnapshotSpan(name, op, attributes, callback) {
379
+ return await withSpan(name, op, {}, callback, attributes);
380
+ }
381
+ async function runOrThrow(sandbox, params, label) {
382
+ const result = await runNonInteractiveCommand(sandbox, params);
383
+ if (result.exitCode === 0) {
384
+ return;
385
+ }
386
+ const stderr = (await result.stderr()).trim();
387
+ const stdout = (await result.stdout()).trim();
388
+ const detail = stderr || stdout || "command failed";
389
+ throw new Error(`${label} failed: ${detail}`);
390
+ }
391
+ async function tryRun(sandbox, params) {
392
+ const result = await runNonInteractiveCommand(sandbox, params);
393
+ if (result.exitCode === 0) {
394
+ return { ok: true };
395
+ }
396
+ const stderr = (await result.stderr()).trim();
397
+ const stdout = (await result.stdout()).trim();
398
+ return { ok: false, detail: stderr || stdout || "command failed" };
399
+ }
400
+ async function installGhCliViaDnf(sandbox) {
401
+ const direct = await tryRun(sandbox, {
402
+ cmd: "dnf",
403
+ args: ["install", "-y", "gh"],
404
+ sudo: true
405
+ });
406
+ if (direct.ok) {
407
+ return;
408
+ }
409
+ const dnf5Repo = await tryRun(sandbox, {
410
+ cmd: "dnf",
411
+ args: [
412
+ "config-manager",
413
+ "addrepo",
414
+ "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
415
+ ],
416
+ sudo: true
417
+ });
418
+ if (!dnf5Repo.ok) {
419
+ await runOrThrow(
420
+ sandbox,
421
+ {
422
+ cmd: "dnf",
423
+ args: ["install", "-y", "dnf-command(config-manager)"],
424
+ sudo: true
425
+ },
426
+ "dnf install dnf-command(config-manager)"
427
+ );
428
+ await runOrThrow(
429
+ sandbox,
430
+ {
431
+ cmd: "dnf",
432
+ args: [
433
+ "config-manager",
434
+ "--add-repo",
435
+ "https://cli.github.com/packages/rpm/gh-cli.repo"
436
+ ],
437
+ sudo: true
438
+ },
439
+ "dnf config-manager --add-repo gh-cli.repo"
440
+ );
441
+ }
442
+ await runOrThrow(
443
+ sandbox,
444
+ {
445
+ cmd: "dnf",
446
+ args: ["install", "-y", "gh", "--repo", "gh-cli"],
447
+ sudo: true
448
+ },
449
+ "dnf install gh --repo gh-cli"
450
+ );
451
+ }
452
+ function runtimeDependencyFilePath(url, sha256) {
453
+ let urlBasename = "package.rpm";
454
+ try {
455
+ const pathname = new URL(url).pathname;
456
+ const segments = pathname.split("/").filter(Boolean);
457
+ const candidate = segments[segments.length - 1];
458
+ if (candidate) {
459
+ urlBasename = candidate;
460
+ }
461
+ } catch {
462
+ }
463
+ const sanitizedBasename = urlBasename.replace(/[^a-zA-Z0-9._-]/g, "_");
464
+ return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
465
+ }
466
+ async function installRuntimeDependencies(sandbox, deps) {
467
+ const systemDeps = deps.filter(
468
+ (dep) => dep.type === "system"
469
+ );
470
+ const npmPackages = deps.filter(
471
+ (dep) => dep.type === "npm"
472
+ ).map((dep) => `${dep.package}@${dep.version}`);
473
+ if (systemDeps.length > 0) {
474
+ await withSnapshotSpan(
475
+ "sandbox.snapshot.install_system",
476
+ "sandbox.snapshot.install.system",
477
+ {
478
+ "app.sandbox.snapshot.install.system_count": systemDeps.length
479
+ },
480
+ async () => {
481
+ for (const dep of systemDeps) {
482
+ if ("url" in dep) {
483
+ const rpmPath = runtimeDependencyFilePath(dep.url, dep.sha256);
484
+ await runOrThrow(
485
+ sandbox,
486
+ {
487
+ cmd: "curl",
488
+ args: ["-fsSL", dep.url, "-o", rpmPath]
489
+ },
490
+ `curl download ${dep.url}`
491
+ );
492
+ const checksumResult = await runNonInteractiveCommand(sandbox, {
493
+ cmd: "sha256sum",
494
+ args: [rpmPath]
495
+ });
496
+ const checksumStdout = (await checksumResult.stdout()).trim();
497
+ const checksumStderr = (await checksumResult.stderr()).trim();
498
+ if (checksumResult.exitCode !== 0) {
499
+ throw new Error(
500
+ `sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
501
+ );
502
+ }
503
+ const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
504
+ if (!actualChecksum) {
505
+ throw new Error("sha256sum produced empty output");
506
+ }
507
+ if (actualChecksum !== dep.sha256) {
508
+ throw new Error(
509
+ `checksum mismatch for ${dep.url}: expected ${dep.sha256}, got ${actualChecksum}`
510
+ );
511
+ }
512
+ await runOrThrow(
513
+ sandbox,
514
+ {
515
+ cmd: "dnf",
516
+ args: ["install", "-y", rpmPath],
517
+ sudo: true
518
+ },
519
+ `dnf install ${dep.url}`
520
+ );
521
+ continue;
522
+ }
523
+ if (dep.package === "gh") {
524
+ await installGhCliViaDnf(sandbox);
525
+ continue;
526
+ }
527
+ await runOrThrow(
528
+ sandbox,
529
+ {
530
+ cmd: "dnf",
531
+ args: ["install", "-y", dep.package],
532
+ sudo: true
533
+ },
534
+ `dnf install ${dep.package}`
535
+ );
536
+ }
537
+ }
538
+ );
539
+ }
540
+ if (npmPackages.length > 0) {
541
+ await withSnapshotSpan(
542
+ "sandbox.snapshot.install_npm",
543
+ "sandbox.snapshot.install.npm",
544
+ {
545
+ "app.sandbox.snapshot.install.npm_count": npmPackages.length
546
+ },
547
+ async () => {
548
+ await runOrThrow(
549
+ sandbox,
550
+ {
551
+ cmd: "npm",
552
+ args: [
553
+ "install",
554
+ "--global",
555
+ "--prefix",
556
+ `${SANDBOX_WORKSPACE_ROOT}/.junior`,
557
+ ...npmPackages
558
+ ]
559
+ },
560
+ "npm install"
561
+ );
562
+ }
563
+ );
564
+ }
565
+ }
566
+ async function runRuntimePostinstall(sandbox, commands) {
567
+ if (commands.length === 0) {
568
+ return;
569
+ }
570
+ await withSnapshotSpan(
571
+ "sandbox.snapshot.runtime_postinstall",
572
+ "sandbox.snapshot.runtime_postinstall",
573
+ {
574
+ "app.sandbox.snapshot.runtime_postinstall.count": commands.length
575
+ },
576
+ async () => {
577
+ for (const command of commands) {
578
+ const result = await runNonInteractiveCommand(sandbox, {
579
+ cmd: command.cmd,
580
+ args: command.args,
581
+ login: true,
582
+ pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
583
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
584
+ });
585
+ if (result.exitCode === 0) {
586
+ continue;
587
+ }
588
+ const stderr = (await result.stderr()).trim();
589
+ const stdout = (await result.stdout()).trim();
590
+ const detail = stderr || stdout || "command failed";
591
+ throw new Error(`runtime-postinstall ${command.cmd} failed: ${detail}`);
592
+ }
593
+ }
594
+ );
595
+ }
596
+ async function createDependencySnapshot(profile, runtime, timeoutMs) {
597
+ return await withSnapshotSpan(
598
+ "sandbox.snapshot.build",
599
+ "sandbox.snapshot.build",
600
+ {
601
+ "app.sandbox.runtime": runtime,
602
+ "app.sandbox.snapshot.dependency_count": profile.dependencyCount
603
+ },
604
+ async () => {
605
+ const sandboxCredentials = getVercelSandboxCredentials();
606
+ const sandbox = await Sandbox.create({
607
+ timeout: timeoutMs,
608
+ runtime,
609
+ ...sandboxCredentials ?? {}
610
+ });
611
+ try {
612
+ await installRuntimeDependencies(sandbox, profile.dependencies);
613
+ await runRuntimePostinstall(sandbox, profile.postinstall);
614
+ return await withSnapshotSpan(
615
+ "sandbox.snapshot.capture",
616
+ "sandbox.snapshot.capture",
617
+ {
618
+ "app.sandbox.snapshot.dependency_count": profile.dependencyCount
619
+ },
620
+ async () => {
621
+ const snapshot = await sandbox.snapshot();
622
+ return snapshot.snapshotId;
623
+ }
624
+ );
625
+ } finally {
626
+ try {
627
+ await sandbox.stop({ blocking: true });
628
+ } catch {
629
+ }
630
+ }
631
+ }
632
+ );
633
+ }
634
+ async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks) {
635
+ const state = getStateAdapter();
636
+ await state.connect();
637
+ const lockKey = profileLockKey(profileHash);
638
+ const tryAcquireLock = async () => await state.acquireLock(lockKey, SNAPSHOT_BUILD_LOCK_TTL_MS);
639
+ let lock = await tryAcquireLock();
640
+ if (lock) {
641
+ try {
642
+ const result = await callback();
643
+ return {
644
+ snapshotId: result.snapshotId,
645
+ source: result.source,
646
+ waitedForLock: false
647
+ };
648
+ } finally {
649
+ await state.releaseLock(lock);
650
+ }
651
+ }
652
+ return await withSnapshotSpan(
653
+ "sandbox.snapshot.lock_wait",
654
+ "sandbox.snapshot.lock_wait",
655
+ {
656
+ "app.sandbox.snapshot.profile_hash": profileHash
657
+ },
658
+ async () => {
659
+ await hooks?.onWaitingForLock?.();
660
+ const waitUntil = Date.now() + SNAPSHOT_WAIT_FOR_LOCK_MS;
661
+ while (Date.now() < waitUntil) {
662
+ const cached2 = await getCachedSnapshot(profileHash);
663
+ if (cached2?.snapshotId && canUseCachedSnapshot(cached2)) {
664
+ return {
665
+ snapshotId: cached2.snapshotId,
666
+ source: "wait_cache",
667
+ waitedForLock: true
668
+ };
669
+ }
670
+ lock = await tryAcquireLock();
671
+ if (lock) {
672
+ try {
673
+ const result = await callback();
674
+ return {
675
+ snapshotId: result.snapshotId,
676
+ source: result.source,
677
+ waitedForLock: true
678
+ };
679
+ } finally {
680
+ await state.releaseLock(lock);
681
+ }
682
+ }
683
+ await sleep(500);
684
+ }
685
+ const cached = await getCachedSnapshot(profileHash);
686
+ if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
687
+ return {
688
+ snapshotId: cached.snapshotId,
689
+ source: "wait_cache",
690
+ waitedForLock: true
691
+ };
692
+ }
693
+ throw new Error("Timed out waiting for snapshot build lock");
694
+ }
695
+ );
696
+ }
697
+ function toResolveOutcome(forceRebuild, source, waitedForLock) {
698
+ if (source === "built") {
699
+ return forceRebuild ? "forced_rebuild" : "rebuilt";
700
+ }
701
+ if (waitedForLock || source === "wait_cache") {
702
+ return "cache_hit_after_lock_wait";
703
+ }
704
+ return "cache_hit";
705
+ }
706
+ function getRebuildReason(params) {
707
+ if (params.forceRebuild) {
708
+ return params.staleSnapshotId ? "snapshot_missing" : "force_rebuild";
709
+ }
710
+ if (params.cached?.snapshotId && params.shouldRebuildCached) {
711
+ return "floating_stale";
712
+ }
713
+ if (!params.cached?.snapshotId) {
714
+ return "cache_miss";
715
+ }
716
+ return void 0;
717
+ }
718
+ async function resolveRuntimeDependencySnapshot(params) {
719
+ return await withSnapshotSpan(
720
+ "sandbox.snapshot.resolve",
721
+ "sandbox.snapshot.resolve",
722
+ {
723
+ "app.sandbox.runtime": params.runtime,
724
+ "app.sandbox.snapshot.force_rebuild": Boolean(params.forceRebuild)
725
+ },
726
+ async () => {
727
+ await params.onProgress?.("resolve_start");
728
+ const resolveStartedAtMs = Date.now();
729
+ const profile = buildDependencyProfile(params.runtime);
730
+ if (!profile) {
731
+ return {
732
+ dependencyCount: 0,
733
+ cacheHit: false,
734
+ resolveOutcome: "no_profile"
735
+ };
736
+ }
737
+ const cached = await getCachedSnapshot(profile.profileHash);
738
+ const cachedNeedsRebuild = Boolean(
739
+ cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
740
+ );
741
+ if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
742
+ await params.onProgress?.("cache_hit");
743
+ return {
744
+ snapshotId: cached.snapshotId,
745
+ profileHash: profile.profileHash,
746
+ dependencyCount: profile.dependencyCount,
747
+ cacheHit: true,
748
+ resolveOutcome: "cache_hit"
749
+ };
750
+ }
751
+ const rebuildReason = getRebuildReason({
752
+ forceRebuild: params.forceRebuild,
753
+ staleSnapshotId: params.staleSnapshotId,
754
+ cached,
755
+ shouldRebuildCached: cachedNeedsRebuild
756
+ });
757
+ const canUseCachedSnapshot = (candidate) => {
758
+ if (params.forceRebuild) {
759
+ if (params.staleSnapshotId) {
760
+ return candidate.snapshotId !== params.staleSnapshotId;
761
+ }
762
+ return candidate.createdAtMs > resolveStartedAtMs;
763
+ }
764
+ return !shouldRebuildCachedSnapshot(profile, candidate);
765
+ };
766
+ const lockResult = await withBuildLock(
767
+ profile.profileHash,
768
+ async () => {
769
+ const latest = await getCachedSnapshot(profile.profileHash);
770
+ if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
771
+ await params.onProgress?.("cache_hit");
772
+ return {
773
+ snapshotId: latest.snapshotId,
774
+ source: "callback_cache"
775
+ };
776
+ }
777
+ await params.onProgress?.("building_snapshot");
778
+ const nextSnapshotId = await createDependencySnapshot(
779
+ profile,
780
+ params.runtime,
781
+ params.timeoutMs
782
+ );
783
+ await setCachedSnapshot({
784
+ profileHash: profile.profileHash,
785
+ snapshotId: nextSnapshotId,
786
+ runtime: params.runtime,
787
+ createdAtMs: Date.now(),
788
+ dependencyCount: profile.dependencyCount
789
+ });
790
+ await params.onProgress?.("build_complete");
791
+ return { snapshotId: nextSnapshotId, source: "built" };
792
+ },
793
+ canUseCachedSnapshot,
794
+ {
795
+ onWaitingForLock: async () => {
796
+ await params.onProgress?.("waiting_for_lock");
797
+ }
798
+ }
799
+ );
800
+ return {
801
+ snapshotId: lockResult.snapshotId,
802
+ profileHash: profile.profileHash,
803
+ dependencyCount: profile.dependencyCount,
804
+ cacheHit: lockResult.source !== "built",
805
+ resolveOutcome: toResolveOutcome(
806
+ Boolean(params.forceRebuild),
807
+ lockResult.source,
808
+ lockResult.waitedForLock
809
+ ),
810
+ ...rebuildReason ? { rebuildReason } : {}
811
+ };
812
+ }
813
+ );
814
+ }
815
+ function isSnapshotMissingError(error) {
816
+ const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
817
+ return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
818
+ }
819
+
820
+ export {
821
+ botConfig,
822
+ getSlackBotToken,
823
+ getSlackSigningSecret,
824
+ getSlackClientId,
825
+ getSlackClientSecret,
826
+ getRuntimeMetadata,
827
+ getConnectedStateContext,
828
+ getStateAdapter,
829
+ disconnectStateAdapter,
830
+ SANDBOX_WORKSPACE_ROOT,
831
+ SANDBOX_SKILLS_ROOT,
832
+ sandboxSkillDir,
833
+ buildNonInteractiveShellScript,
834
+ runNonInteractiveCommand,
835
+ getVercelSandboxCredentials,
836
+ getRuntimeDependencyProfileHash,
837
+ resolveRuntimeDependencySnapshot,
838
+ isSnapshotMissingError
839
+ };