@sentry/junior 0.4.1 → 0.5.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.
@@ -9,604 +9,225 @@ import {
9
9
  discoverProjectRoots
10
10
  } from "./chunk-Z5E25LRN.js";
11
11
 
12
- // src/chat/config.ts
13
- var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
14
- var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
15
- var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
16
- var TURN_TIMEOUT_BUFFER_SECONDS = 20;
17
- function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
18
- const value = Number.parseInt(rawValue ?? "", 10);
19
- if (Number.isNaN(value)) {
20
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
21
- }
22
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
12
+ // src/chat/plugins/registry.ts
13
+ import { readFileSync, readdirSync, statSync } from "fs";
14
+ import path2 from "path";
15
+ import { parse as parseYaml } from "yaml";
16
+
17
+ // src/chat/home.ts
18
+ import fs from "fs";
19
+ import path from "path";
20
+ function homeDir() {
21
+ return resolveHomeDir();
23
22
  }
24
- function resolveQueueCallbackMaxDurationSeconds() {
25
- const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
26
- if (Number.isNaN(value) || value <= 0) {
27
- return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
23
+ function unique(values) {
24
+ return [...new Set(values)];
25
+ }
26
+ function pathExists(targetPath) {
27
+ try {
28
+ fs.accessSync(targetPath);
29
+ return true;
30
+ } catch {
31
+ return false;
28
32
  }
29
- return value;
30
33
  }
31
- function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
32
- const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
33
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
34
+ function hasAnyDataMarkers(appDir) {
35
+ return pathExists(path.join(appDir, "SOUL.md"));
34
36
  }
35
- function buildBotConfig() {
36
- const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
37
- const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
38
- return {
39
- userName: process.env.JUNIOR_BOT_NAME ?? "junior",
40
- modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
41
- fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
42
- turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
43
- };
37
+ function scoreAppCandidate(appDir) {
38
+ let score = 0;
39
+ if (pathExists(path.join(appDir, "SOUL.md"))) {
40
+ score += 4;
41
+ }
42
+ if (pathExists(path.join(appDir, "ABOUT.md"))) {
43
+ score += 2;
44
+ }
45
+ if (pathExists(path.join(appDir, "skills"))) {
46
+ score += 1;
47
+ }
48
+ if (pathExists(path.join(appDir, "plugins"))) {
49
+ score += 1;
50
+ }
51
+ return score;
44
52
  }
45
- var botConfig = buildBotConfig();
46
- function toOptionalTrimmed(value) {
47
- if (!value) {
48
- return void 0;
53
+ function resolveCandidateAppDirs(cwd, projectRoots) {
54
+ const roots = projectRoots ?? discoverProjectRoots(cwd);
55
+ const resolved = [];
56
+ const seen = /* @__PURE__ */ new Set();
57
+ for (const root of roots) {
58
+ const appDir = path.resolve(root, "app");
59
+ if (!pathExists(appDir)) {
60
+ continue;
61
+ }
62
+ if (seen.has(appDir)) {
63
+ continue;
64
+ }
65
+ seen.add(appDir);
66
+ resolved.push(appDir);
49
67
  }
50
- const trimmed = value.trim();
51
- return trimmed.length > 0 ? trimmed : void 0;
68
+ return resolved;
52
69
  }
53
- function getSlackBotToken() {
54
- return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
70
+ function resolveHomeDir(cwd = process.cwd(), options) {
71
+ const resolvedCwd = path.resolve(cwd);
72
+ const directApp = path.resolve(resolvedCwd, "app");
73
+ if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
74
+ return directApp;
75
+ }
76
+ const candidates = resolveCandidateAppDirs(resolvedCwd, options?.projectRoots);
77
+ if (candidates.length === 0) {
78
+ return directApp;
79
+ }
80
+ candidates.sort((left, right) => {
81
+ const leftScore = scoreAppCandidate(left);
82
+ const rightScore = scoreAppCandidate(right);
83
+ if (leftScore !== rightScore) {
84
+ return rightScore - leftScore;
85
+ }
86
+ const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
87
+ const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
88
+ if (leftDistance !== rightDistance) {
89
+ return leftDistance - rightDistance;
90
+ }
91
+ return left.localeCompare(right);
92
+ });
93
+ return candidates[0];
55
94
  }
56
- function getSlackSigningSecret() {
57
- return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
95
+ function resolveContentRoots(subdir) {
96
+ if (subdir === "data") {
97
+ return [homeDir()];
98
+ }
99
+ if (subdir === "skills") {
100
+ return [path.join(homeDir(), "skills"), path.resolve(process.cwd(), "skills")];
101
+ }
102
+ return [path.join(homeDir(), "plugins")];
58
103
  }
59
- function getSlackClientId() {
60
- return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
104
+ function dataRoots() {
105
+ return unique(resolveContentRoots("data"));
61
106
  }
62
- function getSlackClientSecret() {
63
- return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
107
+ function skillRoots() {
108
+ return unique(resolveContentRoots("skills"));
64
109
  }
65
- function hasRedisConfig() {
66
- return Boolean(process.env.REDIS_URL);
110
+ function pluginRoots() {
111
+ return unique(resolveContentRoots("plugins"));
112
+ }
113
+ function soulPathCandidates() {
114
+ const candidates = dataRoots().map((root) => path.join(root, "SOUL.md"));
115
+ return unique(candidates);
67
116
  }
68
117
 
69
- // src/chat/state.ts
70
- import { createRedisState } from "@chat-adapter/state-redis";
71
- import { createMemoryState } from "@chat-adapter/state-memory";
72
- var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
73
- var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
74
- var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
75
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
76
- var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
77
- var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
78
- var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
79
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
80
- var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
81
- local key = KEYS[1]
82
- local nowMs = tonumber(ARGV[1])
83
- local ttlMs = tonumber(ARGV[2])
84
- local payload = ARGV[3]
85
- local current = redis.call("get", key)
86
-
87
- if not current then
88
- redis.call("set", key, payload, "PX", ttlMs)
89
- return 1
90
- end
91
-
92
- local ok, parsed = pcall(cjson.decode, current)
93
- if not ok or type(parsed) ~= "table" then
94
- return 0
95
- end
96
-
97
- local status = parsed["status"]
98
- if status == "failed" then
99
- redis.call("set", key, payload, "PX", ttlMs)
100
- return 3
101
- end
102
- if status ~= "processing" then
103
- return 0
104
- end
105
-
106
- local updatedAtMs = tonumber(parsed["updatedAtMs"])
107
- if not updatedAtMs then
108
- return 0
109
- end
110
-
111
- if updatedAtMs + ttlMs < nowMs then
112
- redis.call("set", key, payload, "PX", ttlMs)
113
- return 2
114
- end
115
-
116
- return 0
117
- `;
118
- var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
119
- local key = KEYS[1]
120
- local ownerToken = ARGV[1]
121
- local ttlMs = tonumber(ARGV[2])
122
- local payload = ARGV[3]
123
- local current = redis.call("get", key)
124
-
125
- if not current then
126
- return 0
127
- end
128
-
129
- local ok, parsed = pcall(cjson.decode, current)
130
- if not ok or type(parsed) ~= "table" then
131
- return 0
132
- end
133
-
134
- local currentOwner = parsed["ownerToken"]
135
- local status = parsed["status"]
136
- if currentOwner ~= ownerToken then
137
- return 0
138
- end
139
- if status ~= "processing" then
140
- return 0
141
- end
118
+ // src/chat/plugins/github-app-broker.ts
119
+ import { createPrivateKey, createSign, randomUUID } from "crypto";
142
120
 
143
- redis.call("set", key, payload, "PX", ttlMs)
144
- return 1
145
- `;
146
- function createQueuedStateAdapter(base) {
147
- const acquireLock = async (threadId, ttlMs) => {
148
- const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
149
- const lock = await base.acquireLock(threadId, effectiveTtlMs);
150
- return lock;
151
- };
152
- return {
153
- connect: () => base.connect(),
154
- disconnect: () => base.disconnect(),
155
- subscribe: (threadId) => base.subscribe(threadId),
156
- unsubscribe: (threadId) => base.unsubscribe(threadId),
157
- isSubscribed: (threadId) => base.isSubscribed(threadId),
158
- acquireLock,
159
- releaseLock: (lock) => base.releaseLock(lock),
160
- extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
161
- get: (key) => base.get(key),
162
- set: (key, value, ttlMs) => base.set(key, value, ttlMs),
163
- setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
164
- delete: (key) => base.delete(key)
165
- };
121
+ // src/chat/plugins/auth-token-placeholder.ts
122
+ var DEFAULT_PLACEHOLDERS = {
123
+ "oauth-bearer": "host_managed_credential",
124
+ "github-app": "ghp_host_managed_credential"
125
+ };
126
+ function resolveAuthTokenPlaceholder(credentials) {
127
+ return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
166
128
  }
167
- function createStateAdapter() {
168
- if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
169
- _redisStateAdapter = void 0;
170
- return createQueuedStateAdapter(createMemoryState());
129
+
130
+ // src/chat/plugins/github-app-broker.ts
131
+ var MAX_LEASE_MS = 60 * 60 * 1e3;
132
+ function normalizeTargetScope(target) {
133
+ const owner = target?.owner?.trim().toLowerCase();
134
+ const repo = target?.repo?.trim().toLowerCase();
135
+ if (!owner || !repo) {
136
+ return "all";
171
137
  }
172
- if (!hasRedisConfig()) {
173
- throw new Error("REDIS_URL is required for durable Slack thread state");
174
- }
175
- const redisState = createRedisState({
176
- url: process.env.REDIS_URL
177
- });
178
- _redisStateAdapter = redisState;
179
- return createQueuedStateAdapter(redisState);
180
- }
181
- var _stateAdapter;
182
- var _redisStateAdapter;
183
- function getRedisStateAdapter() {
184
- if (!_redisStateAdapter) {
185
- getStateAdapter();
186
- }
187
- if (!_redisStateAdapter) {
188
- throw new Error("Redis state adapter is unavailable for this runtime");
189
- }
190
- return _redisStateAdapter;
138
+ return `${owner}/${repo}`;
191
139
  }
192
- function queueMessageKey(rawKey) {
193
- return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
140
+ function base64Url(input) {
141
+ return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
194
142
  }
195
- function parseQueueMessageState(value) {
196
- if (typeof value !== "string") {
197
- return void 0;
143
+ function normalizePrivateKey(raw) {
144
+ let normalized = raw.trim();
145
+ if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
146
+ normalized = normalized.slice(1, -1);
198
147
  }
199
- try {
200
- const parsed = JSON.parse(value);
201
- if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
202
- return void 0;
148
+ normalized = normalized.replace(/\r\n/g, "\n");
149
+ if (normalized.includes("\\n")) {
150
+ normalized = normalized.replace(/\\n/g, "\n");
151
+ }
152
+ if (!normalized.includes("-----BEGIN")) {
153
+ try {
154
+ const decoded = Buffer.from(normalized, "base64").toString("utf8").trim();
155
+ if (decoded.includes("-----BEGIN")) {
156
+ normalized = decoded;
157
+ }
158
+ } catch {
203
159
  }
204
- return {
205
- status: parsed.status,
206
- updatedAtMs: parsed.updatedAtMs,
207
- ...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
208
- ...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
209
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
210
- };
211
- } catch {
212
- return void 0;
213
160
  }
161
+ return normalized;
214
162
  }
215
- function agentTurnSessionKey(conversationId, sessionId) {
216
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
217
- }
218
- function isRecord(value) {
219
- return typeof value === "object" && value !== null;
220
- }
221
- function parseAgentTurnSessionCheckpoint(value) {
222
- if (typeof value !== "string") {
223
- return void 0;
163
+ function getPrivateKey(envName) {
164
+ const raw = process.env[envName];
165
+ if (!raw) {
166
+ throw new Error(`Missing ${envName}`);
224
167
  }
168
+ const normalized = normalizePrivateKey(raw);
169
+ let key;
225
170
  try {
226
- const parsed = JSON.parse(value);
227
- if (!isRecord(parsed)) {
228
- return void 0;
229
- }
230
- const status = parsed.state;
231
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
232
- return void 0;
233
- }
234
- const conversationId = parsed.conversationId;
235
- const sessionId = parsed.sessionId;
236
- const sliceId = parsed.sliceId;
237
- const checkpointVersion = parsed.checkpointVersion;
238
- const updatedAtMs = parsed.updatedAtMs;
239
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
240
- return void 0;
241
- }
242
- return {
243
- checkpointVersion,
244
- conversationId,
245
- sessionId,
246
- sliceId,
247
- state: status,
248
- updatedAtMs,
249
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
250
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
251
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
252
- };
171
+ key = createPrivateKey({ key: normalized, format: "pem" });
253
172
  } catch {
254
- return void 0;
255
- }
256
- }
257
- function getStateAdapter() {
258
- if (!_stateAdapter) {
259
- _stateAdapter = createStateAdapter();
260
- }
261
- return _stateAdapter;
262
- }
263
- async function disconnectStateAdapter() {
264
- if (!_stateAdapter) {
265
- return;
173
+ throw new Error(
174
+ `Invalid ${envName}: expected a PEM-encoded RSA private key (raw PEM, escaped newlines, or base64-encoded PEM)`
175
+ );
266
176
  }
267
- try {
268
- await _stateAdapter.disconnect();
269
- } finally {
270
- _stateAdapter = void 0;
271
- _redisStateAdapter = void 0;
177
+ if (key.asymmetricKeyType !== "rsa") {
178
+ throw new Error(
179
+ `Invalid ${envName}: GitHub App signing requires an RSA private key`
180
+ );
272
181
  }
182
+ return key;
273
183
  }
274
- async function claimQueueIngressDedup(rawKey, ttlMs) {
275
- await getStateAdapter().connect();
276
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
277
- const result = await getRedisStateAdapter().getClient().set(key, "1", {
278
- NX: true,
279
- PX: ttlMs
280
- });
281
- return result === "OK";
282
- }
283
- async function hasQueueIngressDedup(rawKey) {
284
- await getStateAdapter().connect();
285
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
286
- const value = await getRedisStateAdapter().getClient().get(key);
287
- return typeof value === "string" && value.length > 0;
288
- }
289
- async function getQueueMessageProcessingState(rawKey) {
290
- await getStateAdapter().connect();
291
- const state = await getStateAdapter().get(queueMessageKey(rawKey));
292
- return parseQueueMessageState(state);
184
+ function createAppJwt(appId, privateKeyEnv) {
185
+ const now = Math.floor(Date.now() / 1e3);
186
+ const header = { alg: "RS256", typ: "JWT" };
187
+ const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
188
+ const encodedHeader = base64Url(JSON.stringify(header));
189
+ const encodedPayload = base64Url(JSON.stringify(payload));
190
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
191
+ const signer = createSign("RSA-SHA256");
192
+ signer.update(signingInput);
193
+ signer.end();
194
+ const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
195
+ return `${signingInput}.${signature}`;
293
196
  }
294
- async function acquireQueueMessageProcessingOwnership(args) {
295
- await getStateAdapter().connect();
296
- const key = queueMessageKey(args.rawKey);
297
- const nowMs = Date.now();
298
- const payload = JSON.stringify({
299
- status: "processing",
300
- updatedAtMs: nowMs,
301
- ownerToken: args.ownerToken,
302
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
303
- });
304
- const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
305
- keys: [key],
306
- arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
197
+ async function githubRequest(apiBase, path3, params) {
198
+ const response = await fetch(`${apiBase}${path3}`, {
199
+ method: params.method ?? "GET",
200
+ headers: {
201
+ Accept: "application/vnd.github+json",
202
+ Authorization: `Bearer ${params.token}`,
203
+ "X-GitHub-Api-Version": "2022-11-28",
204
+ ...params.body ? { "Content-Type": "application/json" } : {}
205
+ },
206
+ ...params.body ? { body: JSON.stringify(params.body) } : {}
307
207
  });
308
- if (result === 1) {
309
- return "acquired";
310
- }
311
- if (result === 2) {
312
- return "reclaimed";
208
+ const text = await response.text();
209
+ let parsed = void 0;
210
+ if (text) {
211
+ try {
212
+ parsed = JSON.parse(text);
213
+ } catch {
214
+ parsed = void 0;
215
+ }
313
216
  }
314
- if (result === 3) {
315
- return "recovered";
217
+ if (!response.ok) {
218
+ const message = parsed && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
219
+ throw new Error(message);
316
220
  }
317
- return "blocked";
318
- }
319
- async function refreshQueueMessageProcessingOwnership(args) {
320
- await getStateAdapter().connect();
321
- const nowMs = Date.now();
322
- const payload = JSON.stringify({
323
- status: "processing",
324
- updatedAtMs: nowMs,
325
- ownerToken: args.ownerToken,
326
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
327
- });
328
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
329
- keys: [queueMessageKey(args.rawKey)],
330
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
331
- });
332
- return result === 1;
333
- }
334
- async function completeQueueMessageProcessingOwnership(args) {
335
- await getStateAdapter().connect();
336
- const payload = JSON.stringify({
337
- status: "completed",
338
- updatedAtMs: Date.now(),
339
- ownerToken: args.ownerToken,
340
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
341
- });
342
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
343
- keys: [queueMessageKey(args.rawKey)],
344
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
345
- });
346
- return result === 1;
347
- }
348
- async function failQueueMessageProcessingOwnership(args) {
349
- await getStateAdapter().connect();
350
- const payload = JSON.stringify({
351
- status: "failed",
352
- updatedAtMs: Date.now(),
353
- ownerToken: args.ownerToken,
354
- errorMessage: args.errorMessage,
355
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
356
- });
357
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
358
- keys: [queueMessageKey(args.rawKey)],
359
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
360
- });
361
- return result === 1;
221
+ return parsed;
362
222
  }
363
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
364
- await getStateAdapter().connect();
365
- const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
366
- return parseAgentTurnSessionCheckpoint(value);
367
- }
368
- async function upsertAgentTurnSessionCheckpoint(args) {
369
- await getStateAdapter().connect();
370
- const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
371
- const checkpoint = {
372
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
373
- conversationId: args.conversationId,
374
- sessionId: args.sessionId,
375
- sliceId: args.sliceId,
376
- state: args.state,
377
- updatedAtMs: Date.now(),
378
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
379
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
380
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
381
- };
382
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
383
- await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
384
- return checkpoint;
385
- }
386
-
387
- // src/chat/sandbox/runtime-dependency-snapshots.ts
388
- import { createHash } from "crypto";
389
- import { Sandbox } from "@vercel/sandbox";
390
-
391
- // src/chat/plugins/registry.ts
392
- import { readFileSync, readdirSync, statSync } from "fs";
393
- import path2 from "path";
394
- import { parse as parseYaml } from "yaml";
395
-
396
- // src/chat/home.ts
397
- import fs from "fs";
398
- import path from "path";
399
- function homeDir() {
400
- return resolveHomeDir();
401
- }
402
- function unique(values) {
403
- return [...new Set(values)];
404
- }
405
- function pathExists(targetPath) {
406
- try {
407
- fs.accessSync(targetPath);
408
- return true;
409
- } catch {
410
- return false;
411
- }
412
- }
413
- function hasAnyDataMarkers(appDir) {
414
- return pathExists(path.join(appDir, "SOUL.md"));
415
- }
416
- function scoreAppCandidate(appDir) {
417
- let score = 0;
418
- if (pathExists(path.join(appDir, "SOUL.md"))) {
419
- score += 4;
420
- }
421
- if (pathExists(path.join(appDir, "ABOUT.md"))) {
422
- score += 2;
423
- }
424
- if (pathExists(path.join(appDir, "skills"))) {
425
- score += 1;
426
- }
427
- if (pathExists(path.join(appDir, "plugins"))) {
428
- score += 1;
429
- }
430
- return score;
431
- }
432
- function resolveCandidateAppDirs(cwd, projectRoots) {
433
- const roots = projectRoots ?? discoverProjectRoots(cwd);
434
- const resolved = [];
435
- const seen = /* @__PURE__ */ new Set();
436
- for (const root of roots) {
437
- const appDir = path.resolve(root, "app");
438
- if (!pathExists(appDir)) {
439
- continue;
440
- }
441
- if (seen.has(appDir)) {
442
- continue;
443
- }
444
- seen.add(appDir);
445
- resolved.push(appDir);
446
- }
447
- return resolved;
448
- }
449
- function resolveHomeDir(cwd = process.cwd(), options) {
450
- const resolvedCwd = path.resolve(cwd);
451
- const directApp = path.resolve(resolvedCwd, "app");
452
- if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
453
- return directApp;
454
- }
455
- const candidates = resolveCandidateAppDirs(resolvedCwd, options?.projectRoots);
456
- if (candidates.length === 0) {
457
- return directApp;
458
- }
459
- candidates.sort((left, right) => {
460
- const leftScore = scoreAppCandidate(left);
461
- const rightScore = scoreAppCandidate(right);
462
- if (leftScore !== rightScore) {
463
- return rightScore - leftScore;
464
- }
465
- const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
466
- const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
467
- if (leftDistance !== rightDistance) {
468
- return leftDistance - rightDistance;
469
- }
470
- return left.localeCompare(right);
471
- });
472
- return candidates[0];
473
- }
474
- function resolveContentRoots(subdir) {
475
- if (subdir === "data") {
476
- return [homeDir()];
477
- }
478
- if (subdir === "skills") {
479
- return [path.join(homeDir(), "skills"), path.resolve(process.cwd(), "skills")];
480
- }
481
- return [path.join(homeDir(), "plugins")];
482
- }
483
- function dataRoots() {
484
- return unique(resolveContentRoots("data"));
485
- }
486
- function skillRoots() {
487
- return unique(resolveContentRoots("skills"));
488
- }
489
- function pluginRoots() {
490
- return unique(resolveContentRoots("plugins"));
491
- }
492
- function soulPathCandidates() {
493
- const candidates = dataRoots().map((root) => path.join(root, "SOUL.md"));
494
- return unique(candidates);
495
- }
496
-
497
- // src/chat/plugins/github-app-broker.ts
498
- import { createPrivateKey, createSign, randomUUID } from "crypto";
499
-
500
- // src/chat/plugins/auth-token-placeholder.ts
501
- var DEFAULT_PLACEHOLDERS = {
502
- "oauth-bearer": "host_managed_credential",
503
- "github-app": "ghp_host_managed_credential"
504
- };
505
- function resolveAuthTokenPlaceholder(credentials) {
506
- return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
507
- }
508
-
509
- // src/chat/plugins/github-app-broker.ts
510
- var MAX_LEASE_MS = 60 * 60 * 1e3;
511
- function normalizeTargetScope(target) {
512
- const owner = target?.owner?.trim().toLowerCase();
513
- const repo = target?.repo?.trim().toLowerCase();
514
- if (!owner || !repo) {
515
- return "all";
516
- }
517
- return `${owner}/${repo}`;
518
- }
519
- function base64Url(input) {
520
- return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
521
- }
522
- function normalizePrivateKey(raw) {
523
- let normalized = raw.trim();
524
- if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
525
- normalized = normalized.slice(1, -1);
526
- }
527
- normalized = normalized.replace(/\r\n/g, "\n");
528
- if (normalized.includes("\\n")) {
529
- normalized = normalized.replace(/\\n/g, "\n");
530
- }
531
- if (!normalized.includes("-----BEGIN")) {
532
- try {
533
- const decoded = Buffer.from(normalized, "base64").toString("utf8").trim();
534
- if (decoded.includes("-----BEGIN")) {
535
- normalized = decoded;
536
- }
537
- } catch {
538
- }
539
- }
540
- return normalized;
541
- }
542
- function getPrivateKey(envName) {
543
- const raw = process.env[envName];
544
- if (!raw) {
545
- throw new Error(`Missing ${envName}`);
546
- }
547
- const normalized = normalizePrivateKey(raw);
548
- let key;
549
- try {
550
- key = createPrivateKey({ key: normalized, format: "pem" });
551
- } catch {
552
- throw new Error(
553
- `Invalid ${envName}: expected a PEM-encoded RSA private key (raw PEM, escaped newlines, or base64-encoded PEM)`
554
- );
555
- }
556
- if (key.asymmetricKeyType !== "rsa") {
557
- throw new Error(
558
- `Invalid ${envName}: GitHub App signing requires an RSA private key`
559
- );
560
- }
561
- return key;
562
- }
563
- function createAppJwt(appId, privateKeyEnv) {
564
- const now = Math.floor(Date.now() / 1e3);
565
- const header = { alg: "RS256", typ: "JWT" };
566
- const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
567
- const encodedHeader = base64Url(JSON.stringify(header));
568
- const encodedPayload = base64Url(JSON.stringify(payload));
569
- const signingInput = `${encodedHeader}.${encodedPayload}`;
570
- const signer = createSign("RSA-SHA256");
571
- signer.update(signingInput);
572
- signer.end();
573
- const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
574
- return `${signingInput}.${signature}`;
575
- }
576
- async function githubRequest(apiBase, path3, params) {
577
- const response = await fetch(`${apiBase}${path3}`, {
578
- method: params.method ?? "GET",
579
- headers: {
580
- Accept: "application/vnd.github+json",
581
- Authorization: `Bearer ${params.token}`,
582
- "X-GitHub-Api-Version": "2022-11-28",
583
- ...params.body ? { "Content-Type": "application/json" } : {}
584
- },
585
- ...params.body ? { body: JSON.stringify(params.body) } : {}
586
- });
587
- const text = await response.text();
588
- let parsed = void 0;
589
- if (text) {
590
- try {
591
- parsed = JSON.parse(text);
592
- } catch {
593
- parsed = void 0;
594
- }
595
- }
596
- if (!response.ok) {
597
- const message = parsed && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
598
- throw new Error(message);
599
- }
600
- return parsed;
601
- }
602
- function capabilityToPermissions(capability, pluginName) {
603
- if (capability === `${pluginName}.issues.read`) {
604
- return { issues: "read" };
605
- }
606
- if (capability === `${pluginName}.issues.write` || capability === `${pluginName}.issues.comment` || capability === `${pluginName}.labels.write`) {
607
- return { issues: "write" };
608
- }
609
- throw new Error(`Unsupported GitHub capability: ${capability}`);
223
+ function capabilityToPermissions(capability, pluginName) {
224
+ if (capability === `${pluginName}.issues.read`) {
225
+ return { issues: "read" };
226
+ }
227
+ if (capability === `${pluginName}.issues.write` || capability === `${pluginName}.issues.comment` || capability === `${pluginName}.labels.write`) {
228
+ return { issues: "write" };
229
+ }
230
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
610
231
  }
611
232
  function createGitHubAppBroker(manifest, credentials) {
612
233
  const tokenCache = /* @__PURE__ */ new Map();
@@ -1525,119 +1146,498 @@ function loadPlugins() {
1525
1146
  function ensurePluginsLoaded() {
1526
1147
  loadPlugins();
1527
1148
  }
1528
- loadPlugins();
1529
- function getPluginCapabilityProviders() {
1530
- ensurePluginsLoaded();
1531
- return pluginDefinitions.map((plugin) => ({
1532
- provider: plugin.manifest.name,
1533
- capabilities: [...plugin.manifest.capabilities],
1534
- configKeys: [...plugin.manifest.configKeys],
1535
- ...plugin.manifest.target ? { target: { ...plugin.manifest.target } } : {}
1536
- }));
1149
+ loadPlugins();
1150
+ function getPluginCapabilityProviders() {
1151
+ ensurePluginsLoaded();
1152
+ return pluginDefinitions.map((plugin) => ({
1153
+ provider: plugin.manifest.name,
1154
+ capabilities: [...plugin.manifest.capabilities],
1155
+ configKeys: [...plugin.manifest.configKeys],
1156
+ ...plugin.manifest.target ? { target: { ...plugin.manifest.target } } : {}
1157
+ }));
1158
+ }
1159
+ function getPluginProviders() {
1160
+ ensurePluginsLoaded();
1161
+ return [...pluginDefinitions];
1162
+ }
1163
+ function getPluginRuntimeDependencies() {
1164
+ ensurePluginsLoaded();
1165
+ const seen = /* @__PURE__ */ new Set();
1166
+ const deps = [];
1167
+ for (const plugin of pluginDefinitions) {
1168
+ for (const dep of plugin.manifest.runtimeDependencies ?? []) {
1169
+ const key = dep.type === "npm" ? `${dep.type}:${dep.package}:${dep.version}` : "package" in dep ? `${dep.type}:package:${dep.package}` : `${dep.type}:url:${dep.url}:${dep.sha256}`;
1170
+ if (seen.has(key)) {
1171
+ continue;
1172
+ }
1173
+ seen.add(key);
1174
+ deps.push(dep);
1175
+ }
1176
+ }
1177
+ return deps.sort((left, right) => {
1178
+ if (left.type !== right.type) {
1179
+ return left.type.localeCompare(right.type);
1180
+ }
1181
+ const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
1182
+ const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
1183
+ if (leftIdentity !== rightIdentity) {
1184
+ return leftIdentity.localeCompare(rightIdentity);
1185
+ }
1186
+ if (left.type === "npm" && right.type === "npm") {
1187
+ return left.version.localeCompare(right.version);
1188
+ }
1189
+ return 0;
1190
+ });
1191
+ }
1192
+ function getPluginRuntimePostinstall() {
1193
+ ensurePluginsLoaded();
1194
+ const commands = [];
1195
+ for (const plugin of pluginDefinitions) {
1196
+ for (const command of plugin.manifest.runtimePostinstall ?? []) {
1197
+ commands.push({
1198
+ cmd: command.cmd,
1199
+ ...command.args ? { args: [...command.args] } : {},
1200
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
1201
+ });
1202
+ }
1203
+ }
1204
+ return commands;
1205
+ }
1206
+ function getPluginOAuthConfig(provider) {
1207
+ ensurePluginsLoaded();
1208
+ const plugin = pluginsByName.get(provider);
1209
+ if (!plugin?.manifest.oauth) return void 0;
1210
+ const oauth = plugin.manifest.oauth;
1211
+ return {
1212
+ clientIdEnv: oauth.clientIdEnv,
1213
+ clientSecretEnv: oauth.clientSecretEnv,
1214
+ authorizeEndpoint: oauth.authorizeEndpoint,
1215
+ tokenEndpoint: oauth.tokenEndpoint,
1216
+ ...oauth.scope ? { scope: oauth.scope } : {},
1217
+ ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1218
+ ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1219
+ ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1220
+ callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1221
+ };
1222
+ }
1223
+ function getPluginSkillRoots() {
1224
+ ensurePluginsLoaded();
1225
+ return [
1226
+ .../* @__PURE__ */ new Set([
1227
+ ...pluginDefinitions.map((plugin) => plugin.skillsDir),
1228
+ ...packageSkillRoots
1229
+ ])
1230
+ ];
1231
+ }
1232
+ function isPluginProvider(provider) {
1233
+ ensurePluginsLoaded();
1234
+ return pluginsByName.has(provider);
1235
+ }
1236
+ function createPluginBroker(provider, deps) {
1237
+ ensurePluginsLoaded();
1238
+ const plugin = pluginsByName.get(provider);
1239
+ if (!plugin) {
1240
+ throw new Error(`Unknown plugin provider: "${provider}"`);
1241
+ }
1242
+ const { credentials, name } = plugin.manifest;
1243
+ if (!credentials) {
1244
+ throw new Error(`Provider "${name}" has no credentials configured`);
1245
+ }
1246
+ let broker;
1247
+ if (credentials.type === "oauth-bearer") {
1248
+ broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
1249
+ } else if (credentials.type === "github-app") {
1250
+ broker = createGitHubAppBroker(plugin.manifest, credentials);
1251
+ } else {
1252
+ throw new Error(`Unsupported credentials type for plugin "${name}"`);
1253
+ }
1254
+ setSpanAttributes({
1255
+ "app.plugin.name": name,
1256
+ "app.plugin.capabilities": plugin.manifest.capabilities,
1257
+ "app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
1258
+ });
1259
+ return broker;
1260
+ }
1261
+
1262
+ // src/chat/config.ts
1263
+ var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
1264
+ var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
1265
+ var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
1266
+ var TURN_TIMEOUT_BUFFER_SECONDS = 20;
1267
+ function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
1268
+ const value = Number.parseInt(rawValue ?? "", 10);
1269
+ if (Number.isNaN(value)) {
1270
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
1271
+ }
1272
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
1273
+ }
1274
+ function resolveQueueCallbackMaxDurationSeconds() {
1275
+ const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
1276
+ if (Number.isNaN(value) || value <= 0) {
1277
+ return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
1278
+ }
1279
+ return value;
1280
+ }
1281
+ function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
1282
+ const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
1283
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
1284
+ }
1285
+ function buildBotConfig() {
1286
+ const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
1287
+ const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
1288
+ return {
1289
+ userName: process.env.JUNIOR_BOT_NAME ?? "junior",
1290
+ modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
1291
+ fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
1292
+ turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
1293
+ };
1294
+ }
1295
+ var botConfig = buildBotConfig();
1296
+ function toOptionalTrimmed(value) {
1297
+ if (!value) {
1298
+ return void 0;
1299
+ }
1300
+ const trimmed = value.trim();
1301
+ return trimmed.length > 0 ? trimmed : void 0;
1302
+ }
1303
+ function getSlackBotToken() {
1304
+ return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
1305
+ }
1306
+ function getSlackSigningSecret() {
1307
+ return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
1308
+ }
1309
+ function getSlackClientId() {
1310
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
1311
+ }
1312
+ function getSlackClientSecret() {
1313
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
1314
+ }
1315
+ function hasRedisConfig() {
1316
+ return Boolean(process.env.REDIS_URL);
1317
+ }
1318
+
1319
+ // src/chat/state.ts
1320
+ import { createRedisState } from "@chat-adapter/state-redis";
1321
+ import { createMemoryState } from "@chat-adapter/state-memory";
1322
+ var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
1323
+ var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
1324
+ var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
1325
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
1326
+ var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
1327
+ var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1328
+ var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
1329
+ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
1330
+ var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
1331
+ local key = KEYS[1]
1332
+ local nowMs = tonumber(ARGV[1])
1333
+ local ttlMs = tonumber(ARGV[2])
1334
+ local payload = ARGV[3]
1335
+ local current = redis.call("get", key)
1336
+
1337
+ if not current then
1338
+ redis.call("set", key, payload, "PX", ttlMs)
1339
+ return 1
1340
+ end
1341
+
1342
+ local ok, parsed = pcall(cjson.decode, current)
1343
+ if not ok or type(parsed) ~= "table" then
1344
+ return 0
1345
+ end
1346
+
1347
+ local status = parsed["status"]
1348
+ if status == "failed" then
1349
+ redis.call("set", key, payload, "PX", ttlMs)
1350
+ return 3
1351
+ end
1352
+ if status ~= "processing" then
1353
+ return 0
1354
+ end
1355
+
1356
+ local updatedAtMs = tonumber(parsed["updatedAtMs"])
1357
+ if not updatedAtMs then
1358
+ return 0
1359
+ end
1360
+
1361
+ if updatedAtMs + ttlMs < nowMs then
1362
+ redis.call("set", key, payload, "PX", ttlMs)
1363
+ return 2
1364
+ end
1365
+
1366
+ return 0
1367
+ `;
1368
+ var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
1369
+ local key = KEYS[1]
1370
+ local ownerToken = ARGV[1]
1371
+ local ttlMs = tonumber(ARGV[2])
1372
+ local payload = ARGV[3]
1373
+ local current = redis.call("get", key)
1374
+
1375
+ if not current then
1376
+ return 0
1377
+ end
1378
+
1379
+ local ok, parsed = pcall(cjson.decode, current)
1380
+ if not ok or type(parsed) ~= "table" then
1381
+ return 0
1382
+ end
1383
+
1384
+ local currentOwner = parsed["ownerToken"]
1385
+ local status = parsed["status"]
1386
+ if currentOwner ~= ownerToken then
1387
+ return 0
1388
+ end
1389
+ if status ~= "processing" then
1390
+ return 0
1391
+ end
1392
+
1393
+ redis.call("set", key, payload, "PX", ttlMs)
1394
+ return 1
1395
+ `;
1396
+ function createQueuedStateAdapter(base) {
1397
+ const acquireLock = async (threadId, ttlMs) => {
1398
+ const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
1399
+ const lock = await base.acquireLock(threadId, effectiveTtlMs);
1400
+ return lock;
1401
+ };
1402
+ return {
1403
+ connect: () => base.connect(),
1404
+ disconnect: () => base.disconnect(),
1405
+ subscribe: (threadId) => base.subscribe(threadId),
1406
+ unsubscribe: (threadId) => base.unsubscribe(threadId),
1407
+ isSubscribed: (threadId) => base.isSubscribed(threadId),
1408
+ acquireLock,
1409
+ releaseLock: (lock) => base.releaseLock(lock),
1410
+ extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
1411
+ get: (key) => base.get(key),
1412
+ set: (key, value, ttlMs) => base.set(key, value, ttlMs),
1413
+ setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
1414
+ delete: (key) => base.delete(key)
1415
+ };
1416
+ }
1417
+ function createStateAdapter() {
1418
+ if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
1419
+ _redisStateAdapter = void 0;
1420
+ return createQueuedStateAdapter(createMemoryState());
1421
+ }
1422
+ if (!hasRedisConfig()) {
1423
+ throw new Error("REDIS_URL is required for durable Slack thread state");
1424
+ }
1425
+ const redisState = createRedisState({
1426
+ url: process.env.REDIS_URL
1427
+ });
1428
+ _redisStateAdapter = redisState;
1429
+ return createQueuedStateAdapter(redisState);
1430
+ }
1431
+ var _stateAdapter;
1432
+ var _redisStateAdapter;
1433
+ function getRedisStateAdapter() {
1434
+ if (!_redisStateAdapter) {
1435
+ getStateAdapter();
1436
+ }
1437
+ if (!_redisStateAdapter) {
1438
+ throw new Error("Redis state adapter is unavailable for this runtime");
1439
+ }
1440
+ return _redisStateAdapter;
1441
+ }
1442
+ function queueMessageKey(rawKey) {
1443
+ return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
1444
+ }
1445
+ function parseQueueMessageState(value) {
1446
+ if (typeof value !== "string") {
1447
+ return void 0;
1448
+ }
1449
+ try {
1450
+ const parsed = JSON.parse(value);
1451
+ if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
1452
+ return void 0;
1453
+ }
1454
+ return {
1455
+ status: parsed.status,
1456
+ updatedAtMs: parsed.updatedAtMs,
1457
+ ...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
1458
+ ...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
1459
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
1460
+ };
1461
+ } catch {
1462
+ return void 0;
1463
+ }
1464
+ }
1465
+ function agentTurnSessionKey(conversationId, sessionId) {
1466
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
1537
1467
  }
1538
- function getPluginProviders() {
1539
- ensurePluginsLoaded();
1540
- return [...pluginDefinitions];
1468
+ function isRecord(value) {
1469
+ return typeof value === "object" && value !== null;
1541
1470
  }
1542
- function getPluginRuntimeDependencies() {
1543
- ensurePluginsLoaded();
1544
- const seen = /* @__PURE__ */ new Set();
1545
- const deps = [];
1546
- for (const plugin of pluginDefinitions) {
1547
- for (const dep of plugin.manifest.runtimeDependencies ?? []) {
1548
- const key = dep.type === "npm" ? `${dep.type}:${dep.package}:${dep.version}` : "package" in dep ? `${dep.type}:package:${dep.package}` : `${dep.type}:url:${dep.url}:${dep.sha256}`;
1549
- if (seen.has(key)) {
1550
- continue;
1551
- }
1552
- seen.add(key);
1553
- deps.push(dep);
1554
- }
1471
+ function parseAgentTurnSessionCheckpoint(value) {
1472
+ if (typeof value !== "string") {
1473
+ return void 0;
1555
1474
  }
1556
- return deps.sort((left, right) => {
1557
- if (left.type !== right.type) {
1558
- return left.type.localeCompare(right.type);
1475
+ try {
1476
+ const parsed = JSON.parse(value);
1477
+ if (!isRecord(parsed)) {
1478
+ return void 0;
1559
1479
  }
1560
- const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
1561
- const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
1562
- if (leftIdentity !== rightIdentity) {
1563
- return leftIdentity.localeCompare(rightIdentity);
1480
+ const status = parsed.state;
1481
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
1482
+ return void 0;
1564
1483
  }
1565
- if (left.type === "npm" && right.type === "npm") {
1566
- return left.version.localeCompare(right.version);
1484
+ const conversationId = parsed.conversationId;
1485
+ const sessionId = parsed.sessionId;
1486
+ const sliceId = parsed.sliceId;
1487
+ const checkpointVersion = parsed.checkpointVersion;
1488
+ const updatedAtMs = parsed.updatedAtMs;
1489
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
1490
+ return void 0;
1567
1491
  }
1568
- return 0;
1569
- });
1492
+ return {
1493
+ checkpointVersion,
1494
+ conversationId,
1495
+ sessionId,
1496
+ sliceId,
1497
+ state: status,
1498
+ updatedAtMs,
1499
+ piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
1500
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
1501
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
1502
+ };
1503
+ } catch {
1504
+ return void 0;
1505
+ }
1570
1506
  }
1571
- function getPluginRuntimePostinstall() {
1572
- ensurePluginsLoaded();
1573
- const commands = [];
1574
- for (const plugin of pluginDefinitions) {
1575
- for (const command of plugin.manifest.runtimePostinstall ?? []) {
1576
- commands.push({
1577
- cmd: command.cmd,
1578
- ...command.args ? { args: [...command.args] } : {},
1579
- ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
1580
- });
1581
- }
1507
+ function getStateAdapter() {
1508
+ if (!_stateAdapter) {
1509
+ _stateAdapter = createStateAdapter();
1582
1510
  }
1583
- return commands;
1511
+ return _stateAdapter;
1584
1512
  }
1585
- function getPluginOAuthConfig(provider) {
1586
- ensurePluginsLoaded();
1587
- const plugin = pluginsByName.get(provider);
1588
- if (!plugin?.manifest.oauth) return void 0;
1589
- const oauth = plugin.manifest.oauth;
1590
- return {
1591
- clientIdEnv: oauth.clientIdEnv,
1592
- clientSecretEnv: oauth.clientSecretEnv,
1593
- authorizeEndpoint: oauth.authorizeEndpoint,
1594
- tokenEndpoint: oauth.tokenEndpoint,
1595
- ...oauth.scope ? { scope: oauth.scope } : {},
1596
- ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1597
- ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1598
- ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1599
- callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1600
- };
1513
+ async function disconnectStateAdapter() {
1514
+ if (!_stateAdapter) {
1515
+ return;
1516
+ }
1517
+ try {
1518
+ await _stateAdapter.disconnect();
1519
+ } finally {
1520
+ _stateAdapter = void 0;
1521
+ _redisStateAdapter = void 0;
1522
+ }
1601
1523
  }
1602
- function getPluginSkillRoots() {
1603
- ensurePluginsLoaded();
1604
- return [
1605
- .../* @__PURE__ */ new Set([
1606
- ...pluginDefinitions.map((plugin) => plugin.skillsDir),
1607
- ...packageSkillRoots
1608
- ])
1609
- ];
1524
+ async function claimQueueIngressDedup(rawKey, ttlMs) {
1525
+ await getStateAdapter().connect();
1526
+ const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
1527
+ const result = await getRedisStateAdapter().getClient().set(key, "1", {
1528
+ NX: true,
1529
+ PX: ttlMs
1530
+ });
1531
+ return result === "OK";
1610
1532
  }
1611
- function isPluginProvider(provider) {
1612
- ensurePluginsLoaded();
1613
- return pluginsByName.has(provider);
1533
+ async function hasQueueIngressDedup(rawKey) {
1534
+ await getStateAdapter().connect();
1535
+ const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
1536
+ const value = await getRedisStateAdapter().getClient().get(key);
1537
+ return typeof value === "string" && value.length > 0;
1614
1538
  }
1615
- function createPluginBroker(provider, deps) {
1616
- ensurePluginsLoaded();
1617
- const plugin = pluginsByName.get(provider);
1618
- if (!plugin) {
1619
- throw new Error(`Unknown plugin provider: "${provider}"`);
1539
+ async function getQueueMessageProcessingState(rawKey) {
1540
+ await getStateAdapter().connect();
1541
+ const state = await getStateAdapter().get(queueMessageKey(rawKey));
1542
+ return parseQueueMessageState(state);
1543
+ }
1544
+ async function acquireQueueMessageProcessingOwnership(args) {
1545
+ await getStateAdapter().connect();
1546
+ const key = queueMessageKey(args.rawKey);
1547
+ const nowMs = Date.now();
1548
+ const payload = JSON.stringify({
1549
+ status: "processing",
1550
+ updatedAtMs: nowMs,
1551
+ ownerToken: args.ownerToken,
1552
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1553
+ });
1554
+ const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
1555
+ keys: [key],
1556
+ arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
1557
+ });
1558
+ if (result === 1) {
1559
+ return "acquired";
1620
1560
  }
1621
- const { credentials, name } = plugin.manifest;
1622
- if (!credentials) {
1623
- throw new Error(`Provider "${name}" has no credentials configured`);
1561
+ if (result === 2) {
1562
+ return "reclaimed";
1624
1563
  }
1625
- let broker;
1626
- if (credentials.type === "oauth-bearer") {
1627
- broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
1628
- } else if (credentials.type === "github-app") {
1629
- broker = createGitHubAppBroker(plugin.manifest, credentials);
1630
- } else {
1631
- throw new Error(`Unsupported credentials type for plugin "${name}"`);
1564
+ if (result === 3) {
1565
+ return "recovered";
1632
1566
  }
1633
- setSpanAttributes({
1634
- "app.plugin.name": name,
1635
- "app.plugin.capabilities": plugin.manifest.capabilities,
1636
- "app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
1567
+ return "blocked";
1568
+ }
1569
+ async function refreshQueueMessageProcessingOwnership(args) {
1570
+ await getStateAdapter().connect();
1571
+ const nowMs = Date.now();
1572
+ const payload = JSON.stringify({
1573
+ status: "processing",
1574
+ updatedAtMs: nowMs,
1575
+ ownerToken: args.ownerToken,
1576
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1637
1577
  });
1638
- return broker;
1578
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1579
+ keys: [queueMessageKey(args.rawKey)],
1580
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
1581
+ });
1582
+ return result === 1;
1583
+ }
1584
+ async function completeQueueMessageProcessingOwnership(args) {
1585
+ await getStateAdapter().connect();
1586
+ const payload = JSON.stringify({
1587
+ status: "completed",
1588
+ updatedAtMs: Date.now(),
1589
+ ownerToken: args.ownerToken,
1590
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1591
+ });
1592
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1593
+ keys: [queueMessageKey(args.rawKey)],
1594
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
1595
+ });
1596
+ return result === 1;
1597
+ }
1598
+ async function failQueueMessageProcessingOwnership(args) {
1599
+ await getStateAdapter().connect();
1600
+ const payload = JSON.stringify({
1601
+ status: "failed",
1602
+ updatedAtMs: Date.now(),
1603
+ ownerToken: args.ownerToken,
1604
+ errorMessage: args.errorMessage,
1605
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1606
+ });
1607
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1608
+ keys: [queueMessageKey(args.rawKey)],
1609
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
1610
+ });
1611
+ return result === 1;
1612
+ }
1613
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
1614
+ await getStateAdapter().connect();
1615
+ const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
1616
+ return parseAgentTurnSessionCheckpoint(value);
1617
+ }
1618
+ async function upsertAgentTurnSessionCheckpoint(args) {
1619
+ await getStateAdapter().connect();
1620
+ const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
1621
+ const checkpoint = {
1622
+ checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
1623
+ conversationId: args.conversationId,
1624
+ sessionId: args.sessionId,
1625
+ sliceId: args.sliceId,
1626
+ state: args.state,
1627
+ updatedAtMs: Date.now(),
1628
+ piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
1629
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1630
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
1631
+ };
1632
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1633
+ await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
1634
+ return checkpoint;
1639
1635
  }
1640
1636
 
1637
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
1638
+ import { createHash } from "crypto";
1639
+ import { Sandbox } from "@vercel/sandbox";
1640
+
1641
1641
  // src/chat/sandbox/paths.ts
1642
1642
  function normalizeWorkspaceRoot(input) {
1643
1643
  const candidate = (input ?? "").trim();
@@ -2202,6 +2202,8 @@ export {
2202
2202
  parseOAuthTokenResponse,
2203
2203
  getPluginCapabilityProviders,
2204
2204
  getPluginProviders,
2205
+ getPluginRuntimeDependencies,
2206
+ getPluginRuntimePostinstall,
2205
2207
  getPluginOAuthConfig,
2206
2208
  getPluginSkillRoots,
2207
2209
  isPluginProvider,