@sentry/junior 0.203.0 → 0.205.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/dist/agent-D4nu8MQV.js +17521 -0
- package/dist/agent-hooks-DXHpTJ4m.js +4042 -0
- package/dist/agent-runner-C04colea.js +678 -0
- package/dist/api/schema.d.ts +2736 -2639
- package/dist/api/schema.js +4 -169
- package/dist/api.d.ts +23 -17
- package/dist/api.js +3881 -5326
- package/dist/app-Cb776lL8.d.ts +247 -0
- package/dist/app.d.ts +4 -520
- package/dist/app.js +9524 -11875
- package/dist/auth-pause-state-C2P3guS1.js +168 -0
- package/dist/catalog-runtime-CXw3asJD.js +7 -0
- package/dist/checkpoint-CrViJ8Ps.js +2450 -0
- package/dist/cli/chat.d.ts +20 -0
- package/dist/cli/chat.js +229 -336
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +513 -806
- package/dist/cli/env.d.ts +13 -0
- package/dist/cli/env.js +63 -12
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +90 -159
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +43 -64
- package/dist/cli/plugins.d.ts +13 -0
- package/dist/cli/plugins.js +148 -227
- package/dist/cli/run.d.ts +16 -0
- package/dist/cli/run.js +52 -8
- package/dist/cli/snapshot-create.d.ts +4 -0
- package/dist/cli/snapshot-create.js +71 -125
- package/dist/cli/upgrade.d.ts +15 -0
- package/dist/cli/upgrade.js +232 -335
- package/dist/config-BOtJQ7Pc.js +941 -0
- package/dist/context-B45_WqVX.js +298 -0
- package/dist/conversation-privacy-BZRA6Pes.js +182 -0
- package/dist/conversation-work-C6y3T5Hx.js +51 -0
- package/dist/credential-sync-Bep34IB_.js +2661 -0
- package/dist/db-BeyXKJOI.d.ts +5 -0
- package/dist/destinations-BeBiJeEM.js +29 -0
- package/dist/execution-stats-B6lgWxCK.js +373 -0
- package/dist/executor-BlUhFL7_.js +721 -0
- package/dist/experimental-CIXf7Uye.js +46 -0
- package/dist/history-ClE4toxe.d.ts +32 -0
- package/dist/history-CqTuSAdI.js +441 -0
- package/dist/instrumentation.d.ts +3 -3
- package/dist/instrumentation.js +130 -176
- package/dist/job-queue-DLMqIK74.js +63 -0
- package/dist/logging-BMJzdDlP.js +1210 -0
- package/dist/model-profile-CZmVoGdX.js +49 -0
- package/dist/nitro.d.ts +31 -36
- package/dist/nitro.js +275 -456
- package/dist/oauth-callback-server-WwG5_Zgm.js +130 -0
- package/dist/oauth-relay-CdCnu80W.js +62 -0
- package/dist/package-discovery-7a9YqLhE.js +455 -0
- package/dist/pending-auth-CUbXq06G.js +185 -0
- package/dist/plugin-module-BKGOXca2.js +81 -0
- package/dist/plugins-BI9A1vKA.d.ts +95 -0
- package/dist/plugins-egKWjgue.js +99 -0
- package/dist/private-trace-result-BLvBjy_e.js +16 -0
- package/dist/profile-T0tXE4zu.js +861 -0
- package/dist/registry-BA0__8ai.js +1419 -0
- package/dist/reporting-schema-DosTN__Q.js +105 -0
- package/dist/resolve-bvD1ORdv.js +546 -0
- package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
- package/dist/runner-BXYxfIUL.js +392 -0
- package/dist/sandbox-egress-signals-ngEPjSEW.js +46 -0
- package/dist/schema-Rn8-XEaN.js +1140 -0
- package/dist/sentry-CD3SdJl-.js +26 -0
- package/dist/skills-DJOlYdBs.js +316 -0
- package/dist/spawn-h0R1bTEo.js +29 -0
- package/dist/stats-3YZn-Swi.js +2648 -0
- package/dist/store-C5qxF2ac.d.ts +208 -0
- package/dist/task-queue-CoXwd2eq.js +136 -0
- package/dist/task-runner-B2_mJZj0.js +639 -0
- package/dist/thread-state-CqZ1m-es.js +431 -0
- package/dist/turn-execution-fI6PZ0iI.js +47 -0
- package/dist/turn-lifecycle-CcnwrOcp.js +54 -0
- package/dist/usage-B5SfMb3H.js +296 -0
- package/dist/usage-schema-CKaDkH_j.js +20 -0
- package/dist/validation-DfWcvhuS.js +40 -0
- package/dist/vercel-queue-DIkVcfK4.js +167 -0
- package/dist/vercel.d.ts +6 -6
- package/dist/vercel.js +14 -6
- package/dist/version-CnXZ614E.d.ts +5 -0
- package/dist/version-oD-oPrbO.js +8 -0
- package/dist/version.d.ts +2 -4
- package/dist/version.js +2 -6
- package/dist/web-authorization-Crbzlmzo.js +76 -0
- package/dist/web-input-BOuq3oKK.js +494 -0
- package/dist/work-B91LQKHr.js +509 -0
- package/dist/worker-BcRRTe6k.js +625 -0
- package/package.json +9 -9
- package/dist/agent-hooks-GB2VPTNA.js +0 -51
- package/dist/catalog-runtime-UBLIWD3O.js +0 -14
- package/dist/chunk-2LYHWMR4.js +0 -46
- package/dist/chunk-352OWAZH.js +0 -320
- package/dist/chunk-3Y47VMHV.js +0 -6
- package/dist/chunk-3YJSKOWF.js +0 -164
- package/dist/chunk-4KSSKTQ5.js +0 -229
- package/dist/chunk-4UO6FK4G.js +0 -64
- package/dist/chunk-5AHX53NQ.js +0 -717
- package/dist/chunk-5J3E5FWA.js +0 -700
- package/dist/chunk-5PDAG6O3.js +0 -206
- package/dist/chunk-6Y5B5PEX.js +0 -80
- package/dist/chunk-7GWQ5S5R.js +0 -2086
- package/dist/chunk-7H2VZTWN.js +0 -103
- package/dist/chunk-7NL7T6VU.js +0 -406
- package/dist/chunk-A5L27LE4.js +0 -387
- package/dist/chunk-ADIAWL34.js +0 -81
- package/dist/chunk-ASTEMBKJ.js +0 -519
- package/dist/chunk-CEMMLV5L.js +0 -98
- package/dist/chunk-CM5HNV2W.js +0 -131
- package/dist/chunk-CRTRBBRI.js +0 -105
- package/dist/chunk-DA2KGZZI.js +0 -13
- package/dist/chunk-DIB4GX3G.js +0 -8
- package/dist/chunk-DKIJ2RP5.js +0 -68
- package/dist/chunk-DWPN6SCH.js +0 -172
- package/dist/chunk-EDKUFGPL.js +0 -64
- package/dist/chunk-ENY6SKIU.js +0 -1228
- package/dist/chunk-F4MGCZOX.js +0 -2943
- package/dist/chunk-FA6POGLH.js +0 -70
- package/dist/chunk-FSUR7GTG.js +0 -20475
- package/dist/chunk-GKEOHJL5.js +0 -1856
- package/dist/chunk-GMCB6W5Y.js +0 -333
- package/dist/chunk-IE2VPG7O.js +0 -1135
- package/dist/chunk-IZ5B5IHQ.js +0 -3656
- package/dist/chunk-JAL3CH4G.js +0 -27
- package/dist/chunk-KIIEUXD3.js +0 -1764
- package/dist/chunk-KXU4NIL7.js +0 -692
- package/dist/chunk-MTM5IFCY.js +0 -1190
- package/dist/chunk-N7TMSOZH.js +0 -235
- package/dist/chunk-NCEBK6PH.js +0 -36
- package/dist/chunk-NHJU2ZAL.js +0 -56
- package/dist/chunk-P6KTRNVC.js +0 -321
- package/dist/chunk-PDO5BLNM.js +0 -22
- package/dist/chunk-PDRNLLFT.js +0 -3295
- package/dist/chunk-PEQXYSSE.js +0 -45
- package/dist/chunk-PKEX2AQX.js +0 -198
- package/dist/chunk-PSJCGIGO.js +0 -1163
- package/dist/chunk-PZUSUL55.js +0 -506
- package/dist/chunk-R45M4TUS.js +0 -37
- package/dist/chunk-RC745GGZ.js +0 -122
- package/dist/chunk-SP3QKZXJ.js +0 -820
- package/dist/chunk-TXKUKMVO.js +0 -417
- package/dist/chunk-UE4GPWSD.js +0 -931
- package/dist/chunk-UIE3R5XU.js +0 -17
- package/dist/chunk-UXPG6ZIN.js +0 -84
- package/dist/chunk-VFUK3X5B.js +0 -50
- package/dist/chunk-VNMJWREK.js +0 -787
- package/dist/chunk-VZ7CSOX2.js +0 -706
- package/dist/chunk-WCNQJKFT.js +0 -10
- package/dist/chunk-WDZR3QQM.js +0 -135
- package/dist/chunk-WLYVJCHV.js +0 -1433
- package/dist/chunk-WS7NAFTM.js +0 -1520
- package/dist/chunk-X6GDDWQH.js +0 -60
- package/dist/chunk-XPDEYNQI.js +0 -102
- package/dist/chunk-XRW44VGM.js +0 -58
- package/dist/chunk-Y2CM7HXH.js +0 -111
- package/dist/chunk-Y7X25LFY.js +0 -15
- package/dist/conversation-work-VIOQ5ZF3.js +0 -54
- package/dist/db-NGDJV7YH.js +0 -26
- package/dist/discovery-DWVDM7UK.js +0 -44
- package/dist/experimental-P5FZM4CU.js +0 -12
- package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
- package/dist/oauth-relay-5XPVRPYB.js +0 -8
- package/dist/plugins-YTYJMMJF.js +0 -14
- package/dist/runner-ORTNSB2Q.js +0 -522
- package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
- package/dist/sentry-MB3NL3IR.js +0 -42
- package/dist/skills-H376WQOH.js +0 -27
- package/dist/spawn-T43WHLOS.js +0 -42
- package/dist/task-runner-LBZWF32V.js +0 -36
- package/dist/validation-REJLRKI6.js +0 -17
- package/dist/work-3KJ3RE76.js +0 -51
- package/dist/worker-QFEBU72E.js +0 -24
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { R as toOptionalTrimmed, i as getChatConfig } from "./config-BOtJQ7Pc.js";
|
|
2
|
+
import { _ as setSpanAttributes, v as setSpanStatus } from "./logging-BMJzdDlP.js";
|
|
3
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-CXw3asJD.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
6
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
7
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
+
//#region src/chat/sandbox/paths.ts
|
|
9
|
+
function normalizeWorkspaceRoot(input) {
|
|
10
|
+
const candidate = (input ?? "").trim();
|
|
11
|
+
if (!candidate) return "/vercel/sandbox";
|
|
12
|
+
const normalized = candidate.replace(/\/+$/, "");
|
|
13
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
14
|
+
}
|
|
15
|
+
const SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(process.env.VERCEL_SANDBOX_WORKSPACE_DIR);
|
|
16
|
+
const SANDBOX_REPOS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/repos`;
|
|
17
|
+
const SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
|
|
18
|
+
const SANDBOX_DATA_ROOT = `${SANDBOX_WORKSPACE_ROOT}/data`;
|
|
19
|
+
function sandboxSkillDir(skillName) {
|
|
20
|
+
return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
|
|
21
|
+
}
|
|
22
|
+
function sandboxSkillFile(skillName) {
|
|
23
|
+
return `${sandboxSkillDir(skillName)}/SKILL.md`;
|
|
24
|
+
}
|
|
25
|
+
/** Explain how the agent should resolve files and commands owned by a skill. */
|
|
26
|
+
function sandboxSkillPathResolution(skillName) {
|
|
27
|
+
const skillDir = sandboxSkillDir(skillName);
|
|
28
|
+
return `Resolve relative paths in this skill against ${skillDir}. For bash commands from this skill, cd to ${skillDir} first or use absolute paths.`;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/chat/sleep.ts
|
|
32
|
+
/** Wait for a duration, rejecting with the owning signal reason when cancelled. */
|
|
33
|
+
function sleep(ms, signal) {
|
|
34
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (signal.aborted) {
|
|
37
|
+
reject(signal.reason);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const timeoutId = setTimeout(() => {
|
|
41
|
+
signal.removeEventListener("abort", onAbort);
|
|
42
|
+
resolve();
|
|
43
|
+
}, ms);
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
reject(signal.reason);
|
|
47
|
+
};
|
|
48
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/chat/state/locks.ts
|
|
53
|
+
const ACTIVE_LOCK_TTL_MS = 9e4;
|
|
54
|
+
const MUTATION_LOCK_TTL_MS = 1e4;
|
|
55
|
+
/**
|
|
56
|
+
* Acquire a lock for long-running work that the queued state adapter should
|
|
57
|
+
* keep alive while the owning invocation is still making progress.
|
|
58
|
+
*/
|
|
59
|
+
async function acquireActiveLock(state, threadId) {
|
|
60
|
+
return await state.acquireLock(threadId, ACTIVE_LOCK_TTL_MS);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run work under one lock, optionally waiting for the current owner.
|
|
64
|
+
* `keepAlive` is for multi-store mutations that must retain ownership until all
|
|
65
|
+
* side effects finish; callers should still fence each blind write.
|
|
66
|
+
*/
|
|
67
|
+
async function withLock(state, key, run, options = {}) {
|
|
68
|
+
const startedAtMs = Date.now();
|
|
69
|
+
let lock;
|
|
70
|
+
while (true) {
|
|
71
|
+
lock = await state.acquireLock(key, options.ttlMs ?? 9e4);
|
|
72
|
+
if (lock) break;
|
|
73
|
+
if (Date.now() - startedAtMs >= (options.waitMs ?? 0)) return { acquired: false };
|
|
74
|
+
await setTimeout$1(options.retryMs ?? 25, void 0, { ref: false });
|
|
75
|
+
}
|
|
76
|
+
const ttlMs = options.ttlMs ?? 9e4;
|
|
77
|
+
const heartbeat = options.keepAlive ? setInterval(() => {
|
|
78
|
+
state.extendLock(lock, ttlMs);
|
|
79
|
+
}, Math.max(1, Math.floor(ttlMs / 3))) : void 0;
|
|
80
|
+
heartbeat?.unref();
|
|
81
|
+
try {
|
|
82
|
+
return {
|
|
83
|
+
acquired: true,
|
|
84
|
+
value: await run(lock)
|
|
85
|
+
};
|
|
86
|
+
} finally {
|
|
87
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
88
|
+
await state.releaseLock(lock);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Prove lock ownership immediately before a blind write.
|
|
93
|
+
*
|
|
94
|
+
* Whole-record mutations must stop if their lock expired rather than overwrite
|
|
95
|
+
* state committed by a newer owner.
|
|
96
|
+
*/
|
|
97
|
+
async function fenceLock(state, lock, ttlMs = MUTATION_LOCK_TTL_MS) {
|
|
98
|
+
if (!await state.extendLock(lock, ttlMs)) throw new Error(`Lock ownership was lost before write for ${lock.threadId}`);
|
|
99
|
+
}
|
|
100
|
+
/** Run work under the active lock, reporting contention without ambiguity. */
|
|
101
|
+
async function withActiveLock(state, key, run) {
|
|
102
|
+
return await withLock(state, key, run);
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/chat/state/adapter.ts
|
|
106
|
+
const ACTIVE_LOCK_HEARTBEAT_MS = 3e4;
|
|
107
|
+
let stateAdapter;
|
|
108
|
+
let redisStateAdapter;
|
|
109
|
+
/** Lazily connect state operations, including queued-adapter lock heartbeats. */
|
|
110
|
+
function createConnectingStateAdapter(base) {
|
|
111
|
+
const connected = async (operation) => {
|
|
112
|
+
await base.connect();
|
|
113
|
+
return await operation();
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
appendToList: (key, value, options) => connected(() => base.appendToList(key, value, options)),
|
|
117
|
+
connect: () => base.connect(),
|
|
118
|
+
disconnect: () => base.disconnect(),
|
|
119
|
+
subscribe: (threadId) => connected(() => base.subscribe(threadId)),
|
|
120
|
+
unsubscribe: (threadId) => connected(() => base.unsubscribe(threadId)),
|
|
121
|
+
isSubscribed: (threadId) => connected(() => base.isSubscribed(threadId)),
|
|
122
|
+
acquireLock: (threadId, ttlMs) => connected(() => base.acquireLock(threadId, ttlMs)),
|
|
123
|
+
releaseLock: (lock) => connected(() => base.releaseLock(lock)),
|
|
124
|
+
extendLock: (lock, ttlMs) => connected(() => base.extendLock(lock, ttlMs)),
|
|
125
|
+
forceReleaseLock: (threadId) => connected(() => base.forceReleaseLock(threadId)),
|
|
126
|
+
enqueue: (threadId, entry, maxSize) => connected(() => base.enqueue(threadId, entry, maxSize)),
|
|
127
|
+
dequeue: (threadId) => connected(() => base.dequeue(threadId)),
|
|
128
|
+
queueDepth: (threadId) => connected(() => base.queueDepth(threadId)),
|
|
129
|
+
get: (key) => connected(() => base.get(key)),
|
|
130
|
+
getList: (key) => connected(() => base.getList(key)),
|
|
131
|
+
set: (key, value, ttlMs) => connected(() => base.set(key, value, ttlMs)),
|
|
132
|
+
setIfNotExists: (key, value, ttlMs) => connected(() => base.setIfNotExists(key, value, ttlMs)),
|
|
133
|
+
delete: (key) => connected(() => base.delete(key))
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function createPrefixedStateAdapter(base, prefix) {
|
|
137
|
+
const prefixed = (value) => `${prefix}:${value}`;
|
|
138
|
+
const unprefixed = (value) => value.startsWith(`${prefix}:`) ? value.slice(prefix.length + 1) : value;
|
|
139
|
+
const prefixLock = (lock) => ({
|
|
140
|
+
...lock,
|
|
141
|
+
threadId: prefixed(lock.threadId)
|
|
142
|
+
});
|
|
143
|
+
const unprefixLock = (lock) => ({
|
|
144
|
+
...lock,
|
|
145
|
+
threadId: unprefixed(lock.threadId)
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
appendToList: (key, value, options) => base.appendToList(prefixed(key), value, options),
|
|
149
|
+
connect: () => base.connect(),
|
|
150
|
+
disconnect: () => base.disconnect(),
|
|
151
|
+
subscribe: (threadId) => base.subscribe(prefixed(threadId)),
|
|
152
|
+
unsubscribe: (threadId) => base.unsubscribe(prefixed(threadId)),
|
|
153
|
+
isSubscribed: (threadId) => base.isSubscribed(prefixed(threadId)),
|
|
154
|
+
acquireLock: async (threadId, ttlMs) => {
|
|
155
|
+
const lock = await base.acquireLock(prefixed(threadId), ttlMs);
|
|
156
|
+
return lock ? unprefixLock(lock) : null;
|
|
157
|
+
},
|
|
158
|
+
releaseLock: (lock) => base.releaseLock(prefixLock(lock)),
|
|
159
|
+
extendLock: async (lock, ttlMs) => {
|
|
160
|
+
const prefixedLock = prefixLock(lock);
|
|
161
|
+
const extended = await base.extendLock(prefixedLock, ttlMs);
|
|
162
|
+
if (extended) lock.expiresAt = prefixedLock.expiresAt;
|
|
163
|
+
return extended;
|
|
164
|
+
},
|
|
165
|
+
forceReleaseLock: (threadId) => base.forceReleaseLock(prefixed(threadId)),
|
|
166
|
+
enqueue: (threadId, entry, maxSize) => base.enqueue(prefixed(threadId), entry, maxSize),
|
|
167
|
+
dequeue: (threadId) => base.dequeue(prefixed(threadId)),
|
|
168
|
+
queueDepth: (threadId) => base.queueDepth(prefixed(threadId)),
|
|
169
|
+
get: (key) => base.get(prefixed(key)),
|
|
170
|
+
getList: (key) => base.getList(prefixed(key)),
|
|
171
|
+
set: (key, value, ttlMs) => base.set(prefixed(key), value, ttlMs),
|
|
172
|
+
setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(prefixed(key), value, ttlMs),
|
|
173
|
+
delete: (key) => base.delete(prefixed(key))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function createQueuedStateAdapter(base, options) {
|
|
177
|
+
const heartbeats = /* @__PURE__ */ new Map();
|
|
178
|
+
const shouldHeartbeatLock = (ttlMs) => ttlMs === ACTIVE_LOCK_TTL_MS;
|
|
179
|
+
const heartbeatKey = (lock) => `${lock.threadId}:${lock.token}`;
|
|
180
|
+
const stopHeartbeatByKey = (key) => {
|
|
181
|
+
const heartbeat = heartbeats.get(key);
|
|
182
|
+
if (!heartbeat) return;
|
|
183
|
+
clearInterval(heartbeat.timer);
|
|
184
|
+
heartbeats.delete(key);
|
|
185
|
+
};
|
|
186
|
+
const stopHeartbeat = (lock) => {
|
|
187
|
+
stopHeartbeatByKey(heartbeatKey(lock));
|
|
188
|
+
};
|
|
189
|
+
const stopHeartbeatsForThread = (threadId) => {
|
|
190
|
+
for (const [key, heartbeat] of heartbeats) if (heartbeat.lock.threadId === threadId) stopHeartbeatByKey(key);
|
|
191
|
+
};
|
|
192
|
+
const stopAllHeartbeats = () => {
|
|
193
|
+
for (const key of heartbeats.keys()) stopHeartbeatByKey(key);
|
|
194
|
+
};
|
|
195
|
+
const runHeartbeat = async (key) => {
|
|
196
|
+
const heartbeat = heartbeats.get(key);
|
|
197
|
+
if (!heartbeat || heartbeat.inFlight) return;
|
|
198
|
+
heartbeat.inFlight = true;
|
|
199
|
+
try {
|
|
200
|
+
if (Date.now() - heartbeat.startedAtMs >= options.activeLockMaxAgeMs) {
|
|
201
|
+
stopHeartbeatByKey(key);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!await base.extendLock(heartbeat.lock, heartbeat.ttlMs)) {
|
|
205
|
+
stopHeartbeatByKey(key);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
heartbeat.lock.expiresAt = Date.now() + heartbeat.ttlMs;
|
|
209
|
+
} catch {} finally {
|
|
210
|
+
const current = heartbeats.get(key);
|
|
211
|
+
if (current === heartbeat) current.inFlight = false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const startOrUpdateHeartbeat = (lock, ttlMs) => {
|
|
215
|
+
const key = heartbeatKey(lock);
|
|
216
|
+
const existing = heartbeats.get(key);
|
|
217
|
+
if (existing) {
|
|
218
|
+
existing.ttlMs = ttlMs;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const timer = setInterval(() => {
|
|
222
|
+
runHeartbeat(key);
|
|
223
|
+
}, ACTIVE_LOCK_HEARTBEAT_MS);
|
|
224
|
+
timer.unref?.();
|
|
225
|
+
heartbeats.set(key, {
|
|
226
|
+
inFlight: false,
|
|
227
|
+
lock,
|
|
228
|
+
startedAtMs: Date.now(),
|
|
229
|
+
timer,
|
|
230
|
+
ttlMs
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const acquireLock = async (threadId, ttlMs) => {
|
|
234
|
+
const lock = await base.acquireLock(threadId, ttlMs);
|
|
235
|
+
if (lock && shouldHeartbeatLock(ttlMs)) startOrUpdateHeartbeat(lock, ttlMs);
|
|
236
|
+
return lock;
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
appendToList: (key, value, options) => base.appendToList(key, value, options),
|
|
240
|
+
connect: () => base.connect(),
|
|
241
|
+
disconnect: async () => {
|
|
242
|
+
stopAllHeartbeats();
|
|
243
|
+
await base.disconnect();
|
|
244
|
+
},
|
|
245
|
+
subscribe: (threadId) => base.subscribe(threadId),
|
|
246
|
+
unsubscribe: (threadId) => base.unsubscribe(threadId),
|
|
247
|
+
isSubscribed: (threadId) => base.isSubscribed(threadId),
|
|
248
|
+
acquireLock,
|
|
249
|
+
releaseLock: async (lock) => {
|
|
250
|
+
stopHeartbeat(lock);
|
|
251
|
+
await base.releaseLock(lock);
|
|
252
|
+
},
|
|
253
|
+
extendLock: async (lock, ttlMs) => {
|
|
254
|
+
const extended = await base.extendLock(lock, ttlMs);
|
|
255
|
+
if (extended) {
|
|
256
|
+
lock.expiresAt = Date.now() + ttlMs;
|
|
257
|
+
if (shouldHeartbeatLock(ttlMs)) startOrUpdateHeartbeat(lock, ttlMs);
|
|
258
|
+
else stopHeartbeat(lock);
|
|
259
|
+
} else stopHeartbeat(lock);
|
|
260
|
+
return extended;
|
|
261
|
+
},
|
|
262
|
+
forceReleaseLock: async (threadId) => {
|
|
263
|
+
stopHeartbeatsForThread(threadId);
|
|
264
|
+
await base.forceReleaseLock(threadId);
|
|
265
|
+
},
|
|
266
|
+
enqueue: (threadId, entry, maxSize) => base.enqueue(threadId, entry, maxSize),
|
|
267
|
+
dequeue: (threadId) => base.dequeue(threadId),
|
|
268
|
+
queueDepth: (threadId) => base.queueDepth(threadId),
|
|
269
|
+
get: (key) => base.get(key),
|
|
270
|
+
getList: (key) => base.getList(key),
|
|
271
|
+
set: (key, value, ttlMs) => base.set(key, value, ttlMs),
|
|
272
|
+
setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
|
|
273
|
+
delete: (key) => base.delete(key)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function withOptionalPrefix(base, prefix) {
|
|
277
|
+
return prefix ? createPrefixedStateAdapter(base, prefix) : base;
|
|
278
|
+
}
|
|
279
|
+
function createStateAdapter() {
|
|
280
|
+
const config = getChatConfig();
|
|
281
|
+
const activeLockMaxAgeMs = config.bot.turnTimeoutMs + ACTIVE_LOCK_TTL_MS;
|
|
282
|
+
if (config.state.adapter === "memory") {
|
|
283
|
+
redisStateAdapter = void 0;
|
|
284
|
+
return createQueuedStateAdapter(createConnectingStateAdapter(withOptionalPrefix(createMemoryState(), config.state.keyPrefix)), { activeLockMaxAgeMs });
|
|
285
|
+
}
|
|
286
|
+
if (!config.state.redisUrl) throw new Error("REDIS_URL is required for durable Slack thread state");
|
|
287
|
+
const redisState = createRedisState({ url: config.state.redisUrl });
|
|
288
|
+
redisStateAdapter = redisState;
|
|
289
|
+
return createQueuedStateAdapter(createConnectingStateAdapter(withOptionalPrefix(redisState, config.state.keyPrefix)), { activeLockMaxAgeMs });
|
|
290
|
+
}
|
|
291
|
+
function getOptionalRedisStateAdapter() {
|
|
292
|
+
getStateAdapter();
|
|
293
|
+
return redisStateAdapter;
|
|
294
|
+
}
|
|
295
|
+
async function getConnectedStateContext() {
|
|
296
|
+
const adapter = getStateAdapter();
|
|
297
|
+
await adapter.connect();
|
|
298
|
+
return {
|
|
299
|
+
redisStateAdapter: getOptionalRedisStateAdapter(),
|
|
300
|
+
stateAdapter: adapter
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** Return the Redis adapter only when the caller is using the default state adapter. */
|
|
304
|
+
async function getDefaultRedisStateAdapterFor(adapter) {
|
|
305
|
+
if (adapter !== stateAdapter) return;
|
|
306
|
+
const context = await getConnectedStateContext();
|
|
307
|
+
return context.stateAdapter === adapter ? context.redisStateAdapter : void 0;
|
|
308
|
+
}
|
|
309
|
+
function getStateAdapter() {
|
|
310
|
+
if (!stateAdapter) stateAdapter = createStateAdapter();
|
|
311
|
+
return stateAdapter;
|
|
312
|
+
}
|
|
313
|
+
async function disconnectStateAdapter() {
|
|
314
|
+
if (!stateAdapter) return;
|
|
315
|
+
try {
|
|
316
|
+
await stateAdapter.disconnect();
|
|
317
|
+
} finally {
|
|
318
|
+
stateAdapter = void 0;
|
|
319
|
+
redisStateAdapter = void 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/chat/sandbox/noninteractive-command.ts
|
|
324
|
+
const NON_INTERACTIVE_ENV = {
|
|
325
|
+
CI: "1",
|
|
326
|
+
TERM: "dumb",
|
|
327
|
+
NO_COLOR: "1",
|
|
328
|
+
PAGER: "cat",
|
|
329
|
+
GIT_PAGER: "cat",
|
|
330
|
+
GH_PROMPT_DISABLED: "1",
|
|
331
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
332
|
+
GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
|
|
333
|
+
GH_SPINNER_DISABLED: "1",
|
|
334
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
335
|
+
GCM_INTERACTIVE: "never",
|
|
336
|
+
DEBIAN_FRONTEND: "noninteractive",
|
|
337
|
+
GIT_ASKPASS: "/bin/true",
|
|
338
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
339
|
+
GIT_CONFIG_COUNT: "2",
|
|
340
|
+
GIT_CONFIG_KEY_0: "credential.helper",
|
|
341
|
+
GIT_CONFIG_VALUE_0: "",
|
|
342
|
+
GIT_CONFIG_KEY_1: "http.emptyAuth",
|
|
343
|
+
GIT_CONFIG_VALUE_1: "true"
|
|
344
|
+
};
|
|
345
|
+
function shellQuote(value) {
|
|
346
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
347
|
+
}
|
|
348
|
+
function buildEnvExports(options) {
|
|
349
|
+
const lines = [];
|
|
350
|
+
if (options.pathPrefix) lines.push(`export PATH="${options.pathPrefix}"`);
|
|
351
|
+
for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) lines.push(`export ${key}=${shellQuote(value)}`);
|
|
352
|
+
for (const [key, value] of Object.entries(options.env ?? {})) lines.push(`export ${key}=${shellQuote(value)}`);
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
355
|
+
/** Render one argv command as a safely quoted shell script. */
|
|
356
|
+
function buildCommandScript(input) {
|
|
357
|
+
return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(" ");
|
|
358
|
+
}
|
|
359
|
+
/** Build one shell entrypoint that never waits on terminal input. */
|
|
360
|
+
function buildNonInteractiveShellScript(script, options = {}) {
|
|
361
|
+
return [
|
|
362
|
+
...buildEnvExports(options),
|
|
363
|
+
"exec </dev/null",
|
|
364
|
+
script
|
|
365
|
+
].join(" && ");
|
|
366
|
+
}
|
|
367
|
+
/** Wrap argv-style commands so every sandbox subprocess runs in non-interactive mode. */
|
|
368
|
+
function buildNonInteractiveCommand(input) {
|
|
369
|
+
return {
|
|
370
|
+
cmd: "bash",
|
|
371
|
+
args: [input.login ? "-lc" : "-c", buildNonInteractiveShellScript(buildCommandScript(input), {
|
|
372
|
+
env: input.env,
|
|
373
|
+
pathPrefix: input.pathPrefix
|
|
374
|
+
})]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** Run a subprocess through one enforced non-interactive entrypoint. */
|
|
378
|
+
async function runNonInteractiveCommand(sandbox, input) {
|
|
379
|
+
const command = {
|
|
380
|
+
...buildNonInteractiveCommand(input),
|
|
381
|
+
...input.cwd ? { cwd: input.cwd } : void 0,
|
|
382
|
+
...input.signal ? { signal: input.signal } : void 0,
|
|
383
|
+
...input.sudo !== void 0 ? { sudo: input.sudo } : void 0,
|
|
384
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : void 0
|
|
385
|
+
};
|
|
386
|
+
return await sandbox.runCommand(command);
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/chat/sandbox/credentials.ts
|
|
390
|
+
/**
|
|
391
|
+
* Resolve explicit Vercel Sandbox credentials, or return undefined to let
|
|
392
|
+
* the SDK read ambient Vercel auth from its own environment detection.
|
|
393
|
+
*/
|
|
394
|
+
function getVercelSandboxCredentials() {
|
|
395
|
+
const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
|
|
396
|
+
const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
|
|
397
|
+
const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
|
|
398
|
+
if (token && teamId && projectId) return {
|
|
399
|
+
token,
|
|
400
|
+
teamId,
|
|
401
|
+
projectId
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/chat/sandbox/http-error-details.ts
|
|
406
|
+
const DEFAULT_PREVIEW_LIMIT = 512;
|
|
407
|
+
function toTrimmedString(value, maxChars) {
|
|
408
|
+
if (typeof value !== "string") return;
|
|
409
|
+
const normalized = value.trim();
|
|
410
|
+
if (!normalized) return;
|
|
411
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}…` : normalized;
|
|
412
|
+
}
|
|
413
|
+
/** Extract structured HTTP error metadata (status, headers, body preview) for logging. */
|
|
414
|
+
function extractHttpErrorDetails(error, options = {}) {
|
|
415
|
+
const prefix = options.attributePrefix ?? "app.http_error";
|
|
416
|
+
const previewLimit = options.previewLimit ?? DEFAULT_PREVIEW_LIMIT;
|
|
417
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
418
|
+
const err = error ?? {};
|
|
419
|
+
const attributes = {
|
|
420
|
+
"error.type": normalizedError.name || "Error",
|
|
421
|
+
"exception.message": toTrimmedString(normalizedError.message, previewLimit) ?? "HTTP error"
|
|
422
|
+
};
|
|
423
|
+
const response = err.response;
|
|
424
|
+
const statusCode = typeof response?.status === "number" ? response.status : void 0;
|
|
425
|
+
const statusText = toTrimmedString(response?.statusText, previewLimit);
|
|
426
|
+
const responseUrl = toTrimmedString(response?.url, previewLimit);
|
|
427
|
+
const responseText = toTrimmedString(err.text, previewLimit);
|
|
428
|
+
const responseJson = toTrimmedString(err.json && typeof err.json === "object" ? JSON.stringify(err.json) : void 0, previewLimit);
|
|
429
|
+
const contentType = toTrimmedString(response?.headers?.get?.("content-type"), previewLimit);
|
|
430
|
+
const requestIdHeader = toTrimmedString(response?.headers?.get?.("x-request-id"), previewLimit);
|
|
431
|
+
const vercelIdHeader = toTrimmedString(response?.headers?.get?.("x-vercel-id"), previewLimit);
|
|
432
|
+
const requestId = requestIdHeader ?? vercelIdHeader;
|
|
433
|
+
if (statusCode !== void 0) attributes["http.response.status_code"] = statusCode;
|
|
434
|
+
if (statusText) attributes[`${prefix}.status_text`] = statusText;
|
|
435
|
+
if (responseUrl) attributes["url.full"] = responseUrl;
|
|
436
|
+
if (contentType) attributes["http.response.header.content-type"] = [contentType];
|
|
437
|
+
if (requestIdHeader) attributes["http.response.header.x-request-id"] = [requestIdHeader];
|
|
438
|
+
if (vercelIdHeader) attributes["http.response.header.x-vercel-id"] = [vercelIdHeader];
|
|
439
|
+
if (responseText) {
|
|
440
|
+
attributes[`${prefix}.response_text_preview`] = responseText;
|
|
441
|
+
attributes[`${prefix}.response_text_length`] = responseText.length;
|
|
442
|
+
}
|
|
443
|
+
if (responseJson) attributes[`${prefix}.response_json_preview`] = responseJson;
|
|
444
|
+
const summaryParts = [];
|
|
445
|
+
if (statusCode !== void 0) summaryParts.push(`status=${statusCode}`);
|
|
446
|
+
if (statusText) summaryParts.push(`statusText=${statusText}`);
|
|
447
|
+
if (responseUrl) summaryParts.push(`url=${responseUrl}`);
|
|
448
|
+
if (requestId) summaryParts.push(`requestId=${requestId}`);
|
|
449
|
+
for (const field of options.extraFields ?? []) {
|
|
450
|
+
const value = toTrimmedString(err[field.sourceKey], previewLimit);
|
|
451
|
+
if (!value) continue;
|
|
452
|
+
attributes[`${prefix}.${field.attributeKey}`] = value;
|
|
453
|
+
summaryParts.push(`${field.summaryKey ?? field.attributeKey}=${value}`);
|
|
454
|
+
}
|
|
455
|
+
if (responseJson) summaryParts.push(`json=${responseJson}`);
|
|
456
|
+
else if (responseText) summaryParts.push(`text=${responseText}`);
|
|
457
|
+
const searchableText = `${normalizedError.message} ${responseText ?? ""} ${responseJson ?? ""}`.toLowerCase();
|
|
458
|
+
return {
|
|
459
|
+
attributes,
|
|
460
|
+
summary: summaryParts.join(", "),
|
|
461
|
+
searchableText
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/chat/sandbox/snapshot/not-ready-error.ts
|
|
466
|
+
/** Report that a Workspace snapshot is not ready to use. */
|
|
467
|
+
var WorkspaceSnapshotNotReadyError = class extends Error {
|
|
468
|
+
workspaceName;
|
|
469
|
+
constructor(workspaceName) {
|
|
470
|
+
super(`Workspace ${workspaceName} snapshot is not ready`);
|
|
471
|
+
this.name = "WorkspaceSnapshotNotReadyError";
|
|
472
|
+
this.workspaceName = workspaceName;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
/** Return WorkspaceSnapshotNotReadyError from this error or its causes. */
|
|
476
|
+
function getWorkspaceSnapshotNotReadyError(error) {
|
|
477
|
+
const seen = /* @__PURE__ */ new Set();
|
|
478
|
+
let current = error;
|
|
479
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
480
|
+
if (current instanceof WorkspaceSnapshotNotReadyError) return current;
|
|
481
|
+
seen.add(current);
|
|
482
|
+
current = current.cause;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** True when this error or a cause is WorkspaceSnapshotNotReadyError. */
|
|
486
|
+
function isWorkspaceSnapshotNotReadyError(error) {
|
|
487
|
+
return getWorkspaceSnapshotNotReadyError(error) !== void 0;
|
|
488
|
+
}
|
|
489
|
+
/** User message when a Workspace sandbox is still preparing. */
|
|
490
|
+
function workspaceSnapshotNotReadyUserMessage(error) {
|
|
491
|
+
return `The ${error.workspaceName} workspace is still preparing its sandbox. Wait for that preparation to finish, then try again.`;
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/chat/sandbox/errors.ts
|
|
495
|
+
const SANDBOX_ERROR_FIELDS = [{
|
|
496
|
+
sourceKey: "sandboxId",
|
|
497
|
+
attributeKey: "sandbox_id",
|
|
498
|
+
summaryKey: "sandboxId"
|
|
499
|
+
}];
|
|
500
|
+
function getSandboxErrorDetails(error) {
|
|
501
|
+
return extractHttpErrorDetails(error, {
|
|
502
|
+
attributePrefix: "app.sandbox.api_error",
|
|
503
|
+
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function findInErrorChain(error, predicate) {
|
|
507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
508
|
+
let current = error;
|
|
509
|
+
while (current && !seen.has(current)) {
|
|
510
|
+
if (predicate(current)) return true;
|
|
511
|
+
seen.add(current);
|
|
512
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
function isRecord(value) {
|
|
517
|
+
return typeof value === "object" && value !== null;
|
|
518
|
+
}
|
|
519
|
+
function isInvalidSandboxSessionError(error) {
|
|
520
|
+
if (!isRecord(error)) return false;
|
|
521
|
+
if (isRecord(error.json) && error.json.invalidToken === true) return true;
|
|
522
|
+
const response = error.response;
|
|
523
|
+
return isRecord(response) && response.status === 403 && typeof response.url === "string" && response.url.includes("/sandboxes/sessions");
|
|
524
|
+
}
|
|
525
|
+
function getFirstErrorMessage(error) {
|
|
526
|
+
const seen = /* @__PURE__ */ new Set();
|
|
527
|
+
let current = error;
|
|
528
|
+
while (current && !seen.has(current)) {
|
|
529
|
+
if (current instanceof Error) {
|
|
530
|
+
const message = current.message.trim();
|
|
531
|
+
if (message) return message;
|
|
532
|
+
}
|
|
533
|
+
seen.add(current);
|
|
534
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Detect sandbox filesystem operations that can safely ignore existing directories. */
|
|
538
|
+
function isAlreadyExistsError(error) {
|
|
539
|
+
const details = getSandboxErrorDetails(error);
|
|
540
|
+
return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
|
|
541
|
+
}
|
|
542
|
+
/** Detect when the current sandbox session is unavailable and must be reacquired. */
|
|
543
|
+
function isSandboxUnavailableError(error) {
|
|
544
|
+
return findInErrorChain(error, (candidate) => {
|
|
545
|
+
if (isInvalidSandboxSessionError(candidate)) return true;
|
|
546
|
+
const details = getSandboxErrorDetails(candidate);
|
|
547
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
548
|
+
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available") || searchable.includes("stream was closed and is not accepting commands");
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/** Detect a durable sandbox reference that can no longer be restored. */
|
|
552
|
+
function isSandboxMissingError(error) {
|
|
553
|
+
return findInErrorChain(error, (candidate) => {
|
|
554
|
+
const details = getSandboxErrorDetails(candidate);
|
|
555
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
556
|
+
return searchable.includes("status=404") || searchable.includes("status code 404") || searchable.includes("snapshot_not_found");
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
/** Detect transient snapshot boot races so sandbox creation can retry. */
|
|
560
|
+
function isSnapshottingError(error) {
|
|
561
|
+
return findInErrorChain(error, (candidate) => {
|
|
562
|
+
const details = getSandboxErrorDetails(candidate);
|
|
563
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
564
|
+
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/** Detect cancellation so a durable builder stays available for check-in. */
|
|
568
|
+
function isAbortError(error) {
|
|
569
|
+
return findInErrorChain(error, (candidate) => {
|
|
570
|
+
if (typeof DOMException !== "undefined" && candidate instanceof DOMException) return candidate.name === "AbortError";
|
|
571
|
+
if (!(candidate instanceof Error)) return false;
|
|
572
|
+
if (candidate.name === "AbortError") return true;
|
|
573
|
+
return candidate.message.toLowerCase().startsWith("executeagentrun timed out after");
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/** Detect transient Vercel Sandbox API failures that keep a job resumable. */
|
|
577
|
+
function isSandboxApiTransientError(error) {
|
|
578
|
+
return findInErrorChain(error, (candidate) => {
|
|
579
|
+
const details = getSandboxErrorDetails(candidate);
|
|
580
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
581
|
+
if (searchable.includes("internal_server_error") || searchable.includes("status=500") || searchable.includes("status code 500") || searchable.includes("status=502") || searchable.includes("status code 502") || searchable.includes("status=503") || searchable.includes("status code 503") || searchable.includes("status=504") || searchable.includes("status code 504")) return true;
|
|
582
|
+
if (!isRecord(candidate)) return false;
|
|
583
|
+
const response = candidate.response;
|
|
584
|
+
return isRecord(response) && typeof response.status === "number" && response.status >= 500 && response.status < 600;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/** Detect interrupted command streams where no reliable exit status is available. */
|
|
588
|
+
function isSandboxCommandStreamInterruptedError(error) {
|
|
589
|
+
return findInErrorChain(error, (candidate) => {
|
|
590
|
+
if (!(candidate instanceof Error)) return false;
|
|
591
|
+
return candidate.name === "StreamError" && candidate.message.toLowerCase().includes("stream ended before command finished");
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/** Wrap setup failures. Keep WorkspaceSnapshotNotReadyError as-is. */
|
|
595
|
+
function wrapSandboxSetupError(error) {
|
|
596
|
+
const notReady = getWorkspaceSnapshotNotReadyError(error);
|
|
597
|
+
if (notReady) return notReady;
|
|
598
|
+
try {
|
|
599
|
+
const details = getSandboxErrorDetails(error);
|
|
600
|
+
if (details.summary) return new Error(`sandbox setup failed (${details.summary})`, { cause: error });
|
|
601
|
+
} catch {}
|
|
602
|
+
let causeMessage;
|
|
603
|
+
try {
|
|
604
|
+
causeMessage = getFirstErrorMessage(error);
|
|
605
|
+
} catch (cause) {
|
|
606
|
+
causeMessage = cause instanceof Error ? cause.message : void 0;
|
|
607
|
+
}
|
|
608
|
+
if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
|
|
609
|
+
const oneLine = causeMessage.replace(/\s+/g, " ").trim();
|
|
610
|
+
return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
|
|
611
|
+
}
|
|
612
|
+
return new Error("sandbox setup failed", { cause: error });
|
|
613
|
+
}
|
|
614
|
+
/** Record span data and throw one stable sandbox operation error. */
|
|
615
|
+
function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
616
|
+
const details = getSandboxErrorDetails(error);
|
|
617
|
+
setSpanAttributes({
|
|
618
|
+
...details.attributes,
|
|
619
|
+
...includeMissingPath ? { "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent") } : void 0,
|
|
620
|
+
"app.sandbox.success": false
|
|
621
|
+
});
|
|
622
|
+
setSpanStatus("error");
|
|
623
|
+
throw new Error(details.summary ? `${action} failed (${details.summary})` : `${action} failed`, { cause: error });
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/chat/sandbox/docker.ts
|
|
627
|
+
/** Docker daemon config for Vercel sandbox guests (nested containers). */
|
|
628
|
+
const DOCKER_DAEMON_JSON = `{
|
|
629
|
+
"storage-driver": "overlay2",
|
|
630
|
+
"live-restore": false
|
|
631
|
+
}
|
|
632
|
+
`;
|
|
633
|
+
/**
|
|
634
|
+
* Start dockerd when the socket is missing and wait until `docker info` works.
|
|
635
|
+
* Re-execs through sudo so the sandbox user can call it without root.
|
|
636
|
+
*/
|
|
637
|
+
const DOCKER_ENSURE_SCRIPT = `#!/bin/sh
|
|
638
|
+
set -eu
|
|
639
|
+
|
|
640
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
641
|
+
exec sudo -n "$0" "$@"
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
SOCK=/var/run/docker.sock
|
|
645
|
+
PIDFILE=/var/run/docker.pid
|
|
646
|
+
LOG=/var/log/junior-dockerd.log
|
|
647
|
+
|
|
648
|
+
is_ready() {
|
|
649
|
+
[ -S "$SOCK" ] && /usr/bin/docker info >/dev/null 2>&1
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if is_ready; then
|
|
653
|
+
chmod 666 "$SOCK" 2>/dev/null || true
|
|
654
|
+
exit 0
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
mkdir -p /etc/docker /var/lib/docker /var/run /var/log
|
|
658
|
+
if [ ! -f /etc/docker/daemon.json ]; then
|
|
659
|
+
cat > /etc/docker/daemon.json <<'EOF'
|
|
660
|
+
{
|
|
661
|
+
"storage-driver": "overlay2",
|
|
662
|
+
"live-restore": false
|
|
663
|
+
}
|
|
664
|
+
EOF
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
if [ -f "$PIDFILE" ]; then
|
|
668
|
+
oldpid="$(cat "$PIDFILE" 2>/dev/null || true)"
|
|
669
|
+
if [ -n "\${oldpid:-}" ] && ! kill -0 "$oldpid" 2>/dev/null; then
|
|
670
|
+
rm -f "$PIDFILE"
|
|
671
|
+
fi
|
|
672
|
+
fi
|
|
673
|
+
|
|
674
|
+
if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
|
|
675
|
+
# dockerd must outlive this helper; nohup + background is intentional.
|
|
676
|
+
nohup dockerd --host="unix://$SOCK" --pidfile="$PIDFILE" >"$LOG" 2>&1 &
|
|
677
|
+
fi
|
|
678
|
+
|
|
679
|
+
i=0
|
|
680
|
+
while [ "$i" -lt 50 ]; do
|
|
681
|
+
if is_ready; then
|
|
682
|
+
chmod 666 "$SOCK" 2>/dev/null || true
|
|
683
|
+
exit 0
|
|
684
|
+
fi
|
|
685
|
+
i=$((i + 1))
|
|
686
|
+
sleep 0.1
|
|
687
|
+
done
|
|
688
|
+
|
|
689
|
+
echo "junior-ensure-docker: dockerd failed to become ready" >&2
|
|
690
|
+
if [ -f "$LOG" ]; then
|
|
691
|
+
tail -n 50 "$LOG" >&2 || true
|
|
692
|
+
fi
|
|
693
|
+
exit 1
|
|
694
|
+
`;
|
|
695
|
+
/** PATH wrapper so plain `docker` starts the daemon on first use. */
|
|
696
|
+
const DOCKER_CLI_WRAPPER_SCRIPT = `#!/bin/sh
|
|
697
|
+
set -eu
|
|
698
|
+
if command -v junior-ensure-docker >/dev/null 2>&1; then
|
|
699
|
+
junior-ensure-docker
|
|
700
|
+
fi
|
|
701
|
+
exec /usr/bin/docker "$@"
|
|
702
|
+
`;
|
|
703
|
+
/** PATH wrapper so plain `docker-compose` starts the daemon on first use. */
|
|
704
|
+
const DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT = `#!/bin/sh
|
|
705
|
+
set -eu
|
|
706
|
+
if command -v junior-ensure-docker >/dev/null 2>&1; then
|
|
707
|
+
junior-ensure-docker
|
|
708
|
+
fi
|
|
709
|
+
exec /usr/local/libexec/junior-docker-compose "$@"
|
|
710
|
+
`;
|
|
711
|
+
/**
|
|
712
|
+
* Best-effort dockerd start during sandbox prepare.
|
|
713
|
+
* Missing helper is a no-op so tests and base sandboxes stay green.
|
|
714
|
+
*/
|
|
715
|
+
async function ensureDockerDaemon(sandbox, signal) {
|
|
716
|
+
await runNonInteractiveCommand(sandbox, {
|
|
717
|
+
cmd: "sh",
|
|
718
|
+
args: ["-c", "command -v junior-ensure-docker >/dev/null 2>&1 && junior-ensure-docker || true"],
|
|
719
|
+
signal
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/chat/sandbox/runtime-dependencies.ts
|
|
724
|
+
const GLOBAL_RUNTIME_DEPENDENCIES = [
|
|
725
|
+
{
|
|
726
|
+
type: "system",
|
|
727
|
+
package: "docker"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
type: "system",
|
|
731
|
+
package: "spal-release"
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
type: "system",
|
|
735
|
+
package: "ripgrep"
|
|
736
|
+
}
|
|
737
|
+
];
|
|
738
|
+
function writeFileCommand(path, contents, mode = "0644") {
|
|
739
|
+
return [`printf '%s' '${Buffer.from(contents, "utf8").toString("base64")}' | base64 -d > ${path}`, `chmod ${mode} ${path}`].join("\n");
|
|
740
|
+
}
|
|
741
|
+
function writeExecutableCommand(path, contents) {
|
|
742
|
+
return writeFileCommand(path, contents, "0755");
|
|
743
|
+
}
|
|
744
|
+
const GLOBAL_RUNTIME_POSTINSTALL = [{
|
|
745
|
+
cmd: "sh",
|
|
746
|
+
args: ["-c", [
|
|
747
|
+
"set -eu",
|
|
748
|
+
"case \"$(uname -m)\" in",
|
|
749
|
+
" x86_64) asset=\"x86_64\"; sha256=\"7af95166a730b87e172d4fc9aefea8725d3c6c7327d59149267b452114ddb7d4\" ;;",
|
|
750
|
+
" aarch64) asset=\"aarch64\"; sha256=\"49082844b87f03cdcd5f5bbef1ba8c9c897b7a2dfb80cea18d61ec8ca6117e0c\" ;;",
|
|
751
|
+
" *) echo \"unsupported Docker Compose architecture: $(uname -m)\" >&2; exit 1 ;;",
|
|
752
|
+
"esac",
|
|
753
|
+
"tmp=\"$(mktemp)\"",
|
|
754
|
+
"trap 'rm -f \"$tmp\"' EXIT",
|
|
755
|
+
"curl --fail --location --silent --show-error \"https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-linux-${asset}\" --output \"$tmp\"",
|
|
756
|
+
"echo \"${sha256} ${tmp}\" | sha256sum --check --status",
|
|
757
|
+
"install -D -m 0755 \"$tmp\" /usr/local/libexec/junior-docker-compose",
|
|
758
|
+
"mkdir -p /usr/local/lib/docker/cli-plugins /usr/local/libexec /etc/docker /usr/local/bin",
|
|
759
|
+
`mkdir -p ${SANDBOX_WORKSPACE_ROOT}/.junior/bin`,
|
|
760
|
+
writeFileCommand("/etc/docker/daemon.json", DOCKER_DAEMON_JSON),
|
|
761
|
+
writeExecutableCommand("/usr/local/bin/junior-ensure-docker", DOCKER_ENSURE_SCRIPT),
|
|
762
|
+
"ln -sfn /usr/local/libexec/junior-docker-compose /usr/local/lib/docker/cli-plugins/docker-compose",
|
|
763
|
+
writeExecutableCommand("/usr/local/bin/docker-compose", DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT),
|
|
764
|
+
writeExecutableCommand(`${SANDBOX_WORKSPACE_ROOT}/.junior/bin/docker`, DOCKER_CLI_WRAPPER_SCRIPT),
|
|
765
|
+
writeExecutableCommand(`${SANDBOX_WORKSPACE_ROOT}/.junior/bin/docker-compose`, DOCKER_COMPOSE_CLI_WRAPPER_SCRIPT),
|
|
766
|
+
writeExecutableCommand("/usr/local/bin/docker", DOCKER_CLI_WRAPPER_SCRIPT)
|
|
767
|
+
].join("\n")],
|
|
768
|
+
sudo: true
|
|
769
|
+
}];
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/chat/sandbox/snapshot/profile.ts
|
|
772
|
+
const VERSION = 1;
|
|
773
|
+
const DEFAULT_FLOATING_MAX_AGE_MS = 6048e5;
|
|
774
|
+
function isExactNpmVersion(version) {
|
|
775
|
+
return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
|
|
776
|
+
}
|
|
777
|
+
function isFloating(dep) {
|
|
778
|
+
return dep.type === "npm" && !isExactNpmVersion(dep.version);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Merge dependencies for one global install, rejecting competing npm versions.
|
|
782
|
+
*/
|
|
783
|
+
function mergeDependencies(dependencies) {
|
|
784
|
+
const seen = /* @__PURE__ */ new Set();
|
|
785
|
+
const npmVersions = /* @__PURE__ */ new Map();
|
|
786
|
+
const merged = [];
|
|
787
|
+
for (const dependency of dependencies) {
|
|
788
|
+
const key = dependency.type === "npm" ? `${dependency.type}:${dependency.package}:${dependency.version}` : "package" in dependency ? `${dependency.type}:package:${dependency.package}` : `${dependency.type}:url:${dependency.url}:${dependency.sha256}`;
|
|
789
|
+
if (seen.has(key)) continue;
|
|
790
|
+
seen.add(key);
|
|
791
|
+
if (dependency.type === "npm") {
|
|
792
|
+
const version = npmVersions.get(dependency.package);
|
|
793
|
+
if (version !== void 0 && version !== dependency.version) throw new Error(`Conflicting runtime dependency versions for ${dependency.package}: ${version} and ${dependency.version}`);
|
|
794
|
+
npmVersions.set(dependency.package, dependency.version);
|
|
795
|
+
}
|
|
796
|
+
merged.push(dependency);
|
|
797
|
+
}
|
|
798
|
+
return merged;
|
|
799
|
+
}
|
|
800
|
+
function floatingMaxAgeMs() {
|
|
801
|
+
const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
|
|
802
|
+
if (!raw?.trim()) return DEFAULT_FLOATING_MAX_AGE_MS;
|
|
803
|
+
const parsed = Number.parseInt(raw, 10);
|
|
804
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_FLOATING_MAX_AGE_MS;
|
|
805
|
+
}
|
|
806
|
+
/** Locale-independent string order for stable profile hashes. */
|
|
807
|
+
function compareCodePoints(left, right) {
|
|
808
|
+
if (left < right) return -1;
|
|
809
|
+
if (left > right) return 1;
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
812
|
+
function workspaceRecipe(workspace) {
|
|
813
|
+
const repos = [...workspace.repos].map(({ provider, repo }) => ({
|
|
814
|
+
provider,
|
|
815
|
+
repo
|
|
816
|
+
})).sort((left, right) => {
|
|
817
|
+
const provider = compareCodePoints(left.provider, right.provider);
|
|
818
|
+
if (provider !== 0) return provider;
|
|
819
|
+
return compareCodePoints(left.repo, right.repo);
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
id: workspace.id,
|
|
823
|
+
repos,
|
|
824
|
+
setupScript: workspace.setupScript
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/** Build the complete profile that selects one reusable sandbox snapshot. */
|
|
828
|
+
function create(runtime, workspace) {
|
|
829
|
+
const dependencies = mergeDependencies([...GLOBAL_RUNTIME_DEPENDENCIES, ...pluginCatalogRuntime.getRuntimeDependencies()]);
|
|
830
|
+
const pluginPostinstall = pluginCatalogRuntime.getRuntimePostinstall();
|
|
831
|
+
const postinstall = [...GLOBAL_RUNTIME_POSTINSTALL, ...pluginPostinstall];
|
|
832
|
+
if (dependencies.length === 0 && postinstall.length === 0 && !workspace) return null;
|
|
833
|
+
const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
|
|
834
|
+
const floating = dependencies.some((dependency) => isFloating(dependency)) || pluginPostinstall.length > 0 || Boolean(workspace);
|
|
835
|
+
return {
|
|
836
|
+
hash: createHash("sha256").update(JSON.stringify({
|
|
837
|
+
version: VERSION,
|
|
838
|
+
runtime,
|
|
839
|
+
rebuildEpoch,
|
|
840
|
+
dependencies,
|
|
841
|
+
postinstall,
|
|
842
|
+
...workspace ? { workspace: workspaceRecipe(workspace) } : void 0
|
|
843
|
+
})).digest("hex"),
|
|
844
|
+
dependencyCount: dependencies.length,
|
|
845
|
+
floating,
|
|
846
|
+
dependencies,
|
|
847
|
+
postinstall
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/** Return the current dependency profile hash without building its snapshot. */
|
|
851
|
+
function hash(runtime, workspace) {
|
|
852
|
+
return create(runtime, workspace)?.hash;
|
|
853
|
+
}
|
|
854
|
+
/** Decide whether a cached snapshot has outlived a floating profile. */
|
|
855
|
+
function isStale(profile, createdAtMs) {
|
|
856
|
+
if (!profile.floating) return false;
|
|
857
|
+
const maxAgeMs = floatingMaxAgeMs();
|
|
858
|
+
return maxAgeMs === 0 || Date.now() - createdAtMs > maxAgeMs;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
export { sleep as A, getDefaultRedisStateAdapterFor as C, fenceLock as D, acquireActiveLock as E, sandboxSkillDir as F, sandboxSkillFile as I, sandboxSkillPathResolution as L, SANDBOX_REPOS_ROOT as M, SANDBOX_SKILLS_ROOT as N, withActiveLock as O, SANDBOX_WORKSPACE_ROOT as P, disconnectStateAdapter as S, MUTATION_LOCK_TTL_MS as T, workspaceSnapshotNotReadyUserMessage as _, isAbortError as a, buildNonInteractiveShellScript as b, isSandboxCommandStreamInterruptedError as c, isSnapshottingError as d, throwSandboxOperationError as f, isWorkspaceSnapshotNotReadyError as g, getWorkspaceSnapshotNotReadyError as h, ensureDockerDaemon as i, SANDBOX_DATA_ROOT as j, withLock as k, isSandboxMissingError as l, WorkspaceSnapshotNotReadyError as m, hash as n, isAlreadyExistsError as o, wrapSandboxSetupError as p, isStale as r, isSandboxApiTransientError as s, create as t, isSandboxUnavailableError as u, getVercelSandboxCredentials as v, getStateAdapter as w, runNonInteractiveCommand as x, buildCommandScript as y };
|