@sentry/junior 0.66.3 → 0.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,15 @@ import {
8
8
  getStateAdapter,
9
9
  parseSlackThreadId,
10
10
  sandboxSkillDir
11
- } from "./chunk-SAYCFF7O.js";
11
+ } from "./chunk-MT23VNOH.js";
12
12
  import {
13
+ isActorUserId,
13
14
  isRecord,
15
+ logException,
14
16
  logInfo,
15
- logWarn
16
- } from "./chunk-6QWWMZCK.js";
17
+ logWarn,
18
+ parseActorUserId
19
+ } from "./chunk-OIIXZOOC.js";
17
20
  import {
18
21
  sentry_exports
19
22
  } from "./chunk-Z3YD6NHK.js";
@@ -23,755 +26,1788 @@ import {
23
26
  worldPathCandidates
24
27
  } from "./chunk-KVZL5NZS.js";
25
28
 
26
- // src/handlers/health.ts
27
- function GET() {
28
- return Response.json({
29
- status: "ok",
30
- service: "junior",
31
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
32
- });
29
+ // src/chat/plugins/logging.ts
30
+ function createAgentPluginLogger(plugin) {
31
+ return {
32
+ info(message, metadata) {
33
+ logInfo(
34
+ "agent_plugin_log_info",
35
+ {},
36
+ { "app.plugin.name": plugin, ...metadata },
37
+ message
38
+ );
39
+ },
40
+ warn(message, metadata) {
41
+ logWarn(
42
+ "agent_plugin_log_warn",
43
+ {},
44
+ { "app.plugin.name": plugin, ...metadata },
45
+ message
46
+ );
47
+ },
48
+ error(message, metadata) {
49
+ logException(
50
+ new Error(message),
51
+ "agent_plugin_log_error",
52
+ {},
53
+ { "app.plugin.name": plugin, ...metadata },
54
+ message
55
+ );
56
+ }
57
+ };
33
58
  }
34
59
 
35
- // src/chat/prompt.ts
36
- import fs from "fs";
37
- import path from "path";
38
-
39
- // src/chat/turn-context-tag.ts
40
- var TURN_CONTEXT_TAG = "runtime-turn-context";
41
-
42
- // src/chat/interruption-marker.ts
43
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
44
- function getInterruptionMarker() {
45
- return INTERRUPTED_MARKER;
60
+ // src/chat/plugins/state.ts
61
+ import { createHash } from "crypto";
62
+ var MAX_PLUGIN_STATE_KEY_LENGTH = 512;
63
+ function hashKeyPart(value) {
64
+ return createHash("sha256").update(value).digest("hex").slice(0, 32);
46
65
  }
47
-
48
- // src/chat/slack/status-format.ts
49
- var SLACK_STATUS_MAX_LENGTH = 50;
50
- function truncateStatusText(text) {
51
- const trimmed = text.trim();
52
- if (!trimmed) {
53
- return "";
66
+ function pluginStateKey(plugin, key2) {
67
+ return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key2)}`;
68
+ }
69
+ function validatePluginStateKey(key2) {
70
+ if (!key2.trim()) {
71
+ throw new Error("Plugin state key is required");
54
72
  }
55
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
56
- return trimmed;
73
+ if (key2.length > MAX_PLUGIN_STATE_KEY_LENGTH) {
74
+ throw new Error("Plugin state key exceeds the maximum length");
57
75
  }
58
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
59
76
  }
60
-
61
- // src/chat/slack/mrkdwn.ts
62
- function ensureBlockSpacing(text) {
63
- const codeBlockPattern = /^```/;
64
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
65
- const lines = text.split("\n");
66
- const result = [];
67
- let inCodeBlock = false;
68
- for (let i = 0; i < lines.length; i++) {
69
- const line = lines[i];
70
- const isCodeFence = codeBlockPattern.test(line.trimStart());
71
- if (isCodeFence) {
72
- if (!inCodeBlock) {
73
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
74
- if (prev2 !== void 0 && prev2.trim() !== "") {
75
- result.push("");
76
- }
77
- }
78
- inCodeBlock = !inCodeBlock;
79
- result.push(line);
80
- continue;
81
- }
82
- if (inCodeBlock) {
83
- result.push(line);
77
+ function legacyStateKey(key2, options) {
78
+ for (const prefix of options?.legacyStatePrefixes ?? []) {
79
+ const trimmed = prefix.trim();
80
+ if (!trimmed) {
84
81
  continue;
85
82
  }
86
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
87
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
88
- result.push("");
83
+ if (key2 === trimmed || key2.startsWith(`${trimmed}:`)) {
84
+ return key2;
89
85
  }
90
- result.push(line);
91
86
  }
92
- return result.join("\n");
93
- }
94
- function renderSlackMrkdwn(text) {
95
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
96
- normalized = ensureBlockSpacing(normalized);
97
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
87
+ return void 0;
98
88
  }
99
- function normalizeSlackStatusText(text) {
100
- const trimmed = text.trim();
101
- if (!trimmed) {
102
- return "";
103
- }
104
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
89
+ function createPluginState(plugin, options) {
90
+ return {
91
+ async delete(key2) {
92
+ validatePluginStateKey(key2);
93
+ const state = getStateAdapter();
94
+ await state.connect();
95
+ await state.delete(pluginStateKey(plugin, key2));
96
+ const legacyKey = legacyStateKey(key2, options);
97
+ if (legacyKey) {
98
+ await state.delete(legacyKey);
99
+ }
100
+ },
101
+ async get(key2) {
102
+ validatePluginStateKey(key2);
103
+ const state = getStateAdapter();
104
+ await state.connect();
105
+ const value = await state.get(pluginStateKey(plugin, key2));
106
+ if (value !== null && value !== void 0) {
107
+ return value;
108
+ }
109
+ const legacyKey = legacyStateKey(key2, options);
110
+ return legacyKey ? await state.get(legacyKey) ?? void 0 : void 0;
111
+ },
112
+ async set(key2, value, ttlMs) {
113
+ validatePluginStateKey(key2);
114
+ const state = getStateAdapter();
115
+ await state.connect();
116
+ await state.set(pluginStateKey(plugin, key2), value, ttlMs);
117
+ },
118
+ async setIfNotExists(key2, value, ttlMs) {
119
+ validatePluginStateKey(key2);
120
+ const state = getStateAdapter();
121
+ await state.connect();
122
+ const legacyKey = legacyStateKey(key2, options);
123
+ if (legacyKey) {
124
+ const existing = await state.get(legacyKey);
125
+ if (existing !== null && existing !== void 0) {
126
+ return false;
127
+ }
128
+ }
129
+ return await state.setIfNotExists(
130
+ pluginStateKey(plugin, key2),
131
+ value,
132
+ ttlMs
133
+ );
134
+ },
135
+ async withLock(key2, ttlMs, callback) {
136
+ validatePluginStateKey(key2);
137
+ const state = getStateAdapter();
138
+ await state.connect();
139
+ const lockKey = legacyStateKey(key2, options) ?? pluginStateKey(plugin, key2);
140
+ const lock = await state.acquireLock(lockKey, ttlMs);
141
+ if (!lock) {
142
+ throw new Error(`Could not acquire plugin state lock for ${key2}`);
143
+ }
144
+ try {
145
+ return await callback();
146
+ } finally {
147
+ await state.releaseLock(lock);
148
+ }
149
+ }
150
+ };
105
151
  }
106
152
 
107
- // src/chat/slack/output.ts
108
- var MAX_INLINE_CHARS = 2200;
109
- var MAX_INLINE_LINES = 45;
110
- var CONTINUED_MARKER = "\n\n[Continued below]";
111
- function countSlackLines(text) {
112
- if (!text) {
113
- return 0;
153
+ // src/chat/credentials/subject.ts
154
+ import { createHmac, timingSafeEqual } from "crypto";
155
+
156
+ // src/chat/slack/client.ts
157
+ import { WebClient } from "@slack/web-api";
158
+ var SlackActionError = class extends Error {
159
+ code;
160
+ apiError;
161
+ needed;
162
+ provided;
163
+ statusCode;
164
+ requestId;
165
+ errorData;
166
+ retryAfterSeconds;
167
+ detail;
168
+ detailLine;
169
+ detailRule;
170
+ constructor(message, code, options = {}) {
171
+ super(message);
172
+ this.name = "SlackActionError";
173
+ this.code = code;
174
+ this.apiError = options.apiError;
175
+ this.needed = options.needed;
176
+ this.provided = options.provided;
177
+ this.statusCode = options.statusCode;
178
+ this.requestId = options.requestId;
179
+ this.errorData = options.errorData;
180
+ this.retryAfterSeconds = options.retryAfterSeconds;
181
+ this.detail = options.detail;
182
+ this.detailLine = options.detailLine;
183
+ this.detailRule = options.detailRule;
114
184
  }
115
- return text.split("\n").length;
116
- }
117
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
118
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
119
- }
120
- function findSplitIndex(text, maxChars) {
121
- if (text.length <= maxChars) {
122
- return text.length;
185
+ };
186
+ function serializeSlackErrorData(data) {
187
+ if (!data || typeof data !== "object") {
188
+ return void 0;
123
189
  }
124
- const bounded = text.slice(0, maxChars);
125
- const newlineIndex = bounded.lastIndexOf("\n");
126
- if (newlineIndex > 0) {
127
- return newlineIndex;
190
+ const filtered = Object.fromEntries(
191
+ Object.entries(data).filter(
192
+ ([key2]) => key2 !== "error"
193
+ )
194
+ );
195
+ if (Object.keys(filtered).length === 0) {
196
+ return void 0;
128
197
  }
129
- const spaceIndex = bounded.lastIndexOf(" ");
130
- if (spaceIndex > 0) {
131
- return spaceIndex;
198
+ try {
199
+ const serialized = JSON.stringify(filtered);
200
+ return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
201
+ } catch {
202
+ return void 0;
132
203
  }
133
- return maxChars;
134
204
  }
135
- function splitByLineBudget(text, maxLines) {
136
- if (maxLines <= 0) {
137
- return "";
205
+ function getHeaderString(headers, name) {
206
+ if (!headers || typeof headers !== "object") {
207
+ return void 0;
138
208
  }
139
- const lines = text.split("\n");
140
- if (lines.length <= maxLines) {
141
- return text;
209
+ const key2 = name.toLowerCase();
210
+ const entries = headers;
211
+ for (const [entryKey, value] of Object.entries(entries)) {
212
+ if (entryKey.toLowerCase() !== key2) continue;
213
+ if (typeof value === "string") return value;
214
+ if (Array.isArray(value)) {
215
+ const first = value.find((entry) => typeof entry === "string");
216
+ return typeof first === "string" ? first : void 0;
217
+ }
142
218
  }
143
- return lines.slice(0, maxLines).join("\n");
144
- }
145
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
146
- return {
147
- maxChars: Math.max(1, maxChars - suffix.length),
148
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
149
- };
219
+ return void 0;
150
220
  }
151
- function forceSplitBudget(text, budget) {
152
- const lineCount = countSlackLines(text);
153
- return {
154
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
155
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
221
+ function parseSlackCanvasDetail(detail) {
222
+ if (typeof detail !== "string") {
223
+ return {};
224
+ }
225
+ const trimmed = detail.trim();
226
+ if (!trimmed) {
227
+ return {};
228
+ }
229
+ const parsed = {
230
+ detail: trimmed
156
231
  };
157
- }
158
- function getFenceContinuation(text) {
159
- let open;
160
- for (const line of text.split("\n")) {
161
- const trimmed = line.trimStart();
162
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
163
- if (!openerMatch) {
164
- continue;
165
- }
166
- if (!open) {
167
- open = {
168
- fence: openerMatch[1],
169
- openerLine: trimmed
170
- };
171
- continue;
172
- }
173
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
174
- open = void 0;
232
+ const lineMatch = trimmed.match(/line\s+(\d+):/i);
233
+ if (lineMatch) {
234
+ const line = Number.parseInt(lineMatch[1] ?? "", 10);
235
+ if (Number.isFinite(line)) {
236
+ parsed.detailLine = line;
175
237
  }
176
238
  }
177
- if (!open) {
178
- return null;
239
+ if (/unsupported heading depth/i.test(trimmed)) {
240
+ parsed.detailRule = "unsupported_heading_depth";
179
241
  }
180
- return {
181
- closeSuffix: text.endsWith("\n") ? open.fence : `
182
- ${open.fence}`,
183
- reopenPrefix: `${open.openerLine}
184
- `
185
- };
186
- }
187
- function appendSlackSuffix(text, marker) {
188
- const carryover = getFenceContinuation(text);
189
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
242
+ return parsed;
190
243
  }
191
- function stripTrailingContinuationMarker(text) {
192
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
244
+ var client = null;
245
+ function normalizeSlackConversationId(channelId) {
246
+ if (!channelId) return void 0;
247
+ const trimmed = channelId.trim();
248
+ if (!trimmed) return void 0;
249
+ if (!trimmed.startsWith("slack:")) {
250
+ return trimmed;
251
+ }
252
+ const parts = trimmed.split(":");
253
+ return parts[1]?.trim() || void 0;
193
254
  }
194
- function takeSlackContinuationChunk(text, budget) {
195
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
196
- if (!rest) {
197
- ({ prefix, rest } = takeSlackInlinePrefix(
198
- text,
199
- forceSplitBudget(text, budget)
200
- ));
255
+ function getClient() {
256
+ if (client) return client;
257
+ const token = getSlackBotToken();
258
+ if (!token) {
259
+ throw new SlackActionError(
260
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
261
+ "missing_token"
262
+ );
201
263
  }
202
- let carryover = rest ? getFenceContinuation(prefix) : null;
203
- if (!carryover) {
204
- return { prefix, rest };
264
+ client = new WebClient(token);
265
+ return client;
266
+ }
267
+ function mapSlackError(error) {
268
+ if (error instanceof SlackActionError) {
269
+ return error;
205
270
  }
206
- const carryoverBudget = reserveInlineBudgetForSuffix(
207
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
208
- );
209
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
210
- if (!rest) {
211
- ({ prefix, rest } = takeSlackInlinePrefix(
212
- text,
213
- forceSplitBudget(text, carryoverBudget)
214
- ));
271
+ const candidate = error;
272
+ const apiError = candidate.data?.error;
273
+ const message = candidate.message ?? "Slack action failed";
274
+ const baseOptions = {
275
+ apiError,
276
+ statusCode: candidate.statusCode,
277
+ requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
278
+ errorData: serializeSlackErrorData(candidate.data),
279
+ ...parseSlackCanvasDetail(candidate.data?.detail)
280
+ };
281
+ if (apiError === "missing_scope") {
282
+ return new SlackActionError(message, "missing_scope", {
283
+ ...baseOptions,
284
+ needed: candidate.data?.needed,
285
+ provided: candidate.data?.provided
286
+ });
215
287
  }
216
- carryover = rest ? getFenceContinuation(prefix) : null;
217
- if (!carryover) {
218
- return { prefix, rest };
288
+ if (apiError === "not_in_channel") {
289
+ return new SlackActionError(message, "not_in_channel", baseOptions);
219
290
  }
220
- return {
221
- prefix,
222
- rest: `${carryover.reopenPrefix}${rest}`
223
- };
224
- }
225
- function takeSlackContinuationPrefix(text, options) {
226
- const budget = {
227
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
228
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
229
- };
230
- const { prefix, rest } = (() => {
231
- if (options?.forceSplit) {
232
- return takeSlackContinuationChunk(text, budget);
233
- }
234
- const initial = takeSlackInlinePrefix(text, budget);
235
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
236
- })();
237
- return {
238
- prefix,
239
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
240
- rest
241
- };
242
- }
243
- function takeSlackInlinePrefix(text, options) {
244
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
245
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
246
- const normalized = text.replace(/\r\n?/g, "\n");
247
- if (!normalized) {
248
- return { prefix: "", rest: "" };
291
+ if (apiError === "already_reacted") {
292
+ return new SlackActionError(message, "already_reacted", baseOptions);
249
293
  }
250
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
251
- return { prefix: normalized, rest: "" };
294
+ if (apiError === "no_reaction") {
295
+ return new SlackActionError(message, "no_reaction", baseOptions);
252
296
  }
253
- const lineBounded = splitByLineBudget(normalized, maxLines);
254
- const cutIndex = findSplitIndex(lineBounded, maxChars);
255
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
256
- if (prefix) {
257
- return {
258
- prefix,
259
- rest: normalized.slice(prefix.length).trimStart()
260
- };
297
+ if (apiError === "invalid_arguments") {
298
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
261
299
  }
262
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
263
- return {
264
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
265
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
266
- };
267
- }
268
- function splitSlackReplyText(text, options) {
269
- const normalized = renderSlackMrkdwn(text);
270
- if (!normalized) {
271
- return [];
300
+ if (apiError === "invalid_cursor") {
301
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
272
302
  }
273
- const chunks = [];
274
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
275
- let remaining = normalized;
276
- while (remaining) {
277
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
278
- if (fitsFinalChunk) {
279
- chunks.push(
280
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
281
- );
282
- break;
283
- }
284
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
285
- ...continuationBudget,
286
- forceSplit: true
287
- });
288
- chunks.push(renderedPrefix);
289
- remaining = rest;
303
+ if (apiError === "invalid_name") {
304
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
290
305
  }
291
- if (chunks.length === 2) {
292
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
306
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
307
+ return new SlackActionError(message, "not_found", baseOptions);
293
308
  }
294
- return chunks;
295
- }
296
- function getSlackContinuationBudget() {
297
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
298
- }
299
- function buildSlackOutputMessage(text, files) {
300
- const normalized = renderSlackMrkdwn(text);
301
- const fileCount = files?.length ?? 0;
302
- if (!normalized) {
303
- if (fileCount > 0) {
304
- return {
305
- raw: "",
306
- files
307
- };
308
- }
309
- throw new Error(
310
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
311
- );
309
+ if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
310
+ return new SlackActionError(message, "feature_unavailable", baseOptions);
312
311
  }
313
- return {
314
- markdown: normalized,
315
- files
316
- };
317
- }
318
- var slackOutputPolicy = {
319
- maxInlineChars: MAX_INLINE_CHARS,
320
- maxInlineLines: MAX_INLINE_LINES
321
- };
322
-
323
- // src/chat/xml.ts
324
- function escapeXml(value) {
325
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
312
+ if (apiError === "canvas_creation_failed") {
313
+ return new SlackActionError(message, "canvas_creation_failed", baseOptions);
314
+ }
315
+ if (apiError === "canvas_editing_failed") {
316
+ return new SlackActionError(message, "canvas_editing_failed", baseOptions);
317
+ }
318
+ if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
319
+ return new SlackActionError(message, "rate_limited", {
320
+ ...baseOptions,
321
+ retryAfterSeconds: candidate.retryAfter
322
+ });
323
+ }
324
+ return new SlackActionError(message, "internal_error", baseOptions);
326
325
  }
327
-
328
- // src/chat/prompt.ts
329
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
330
- function getLoggedMarkdownFiles() {
331
- const globalState = globalThis;
332
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
333
- return globalState.__juniorLoggedMarkdownFiles;
326
+ function sleep(ms) {
327
+ return new Promise((resolve) => setTimeout(resolve, ms));
334
328
  }
335
- function loadOptionalMarkdownFile(candidates, fileName) {
336
- for (const resolved of candidates) {
329
+ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
330
+ let attempt = 0;
331
+ while (attempt < maxAttempts) {
332
+ attempt += 1;
337
333
  try {
338
- const raw = fs.readFileSync(resolved, "utf8").trim();
339
- if (raw.length > 0) {
340
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
341
- const logKey = `${fileName}:${resolved}`;
342
- if (!loggedMarkdownFiles.has(logKey)) {
343
- loggedMarkdownFiles.add(logKey);
344
- logInfo(
345
- `${fileName.toLowerCase()}_loaded`,
346
- {},
347
- {
348
- "file.path": resolved
349
- },
350
- `Loaded ${fileName}`
351
- );
352
- }
353
- return raw;
334
+ return await task();
335
+ } catch (error) {
336
+ const mapped = mapSlackError(error);
337
+ const isRetryable = mapped.code === "rate_limited";
338
+ const baseLogAttributes = {
339
+ "app.slack.action": context.action ?? "unknown",
340
+ "app.slack.error_code": mapped.code,
341
+ ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
342
+ ...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
343
+ ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
344
+ ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
345
+ ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
346
+ ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
347
+ ...context.attributes ?? {}
348
+ };
349
+ if (!isRetryable || attempt >= maxAttempts) {
350
+ logWarn(
351
+ "slack_action_failed",
352
+ {},
353
+ {
354
+ ...baseLogAttributes,
355
+ ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
356
+ },
357
+ "Slack action failed"
358
+ );
359
+ throw mapped;
354
360
  }
355
- } catch {
356
- continue;
361
+ logWarn(
362
+ "slack_action_retrying",
363
+ {},
364
+ {
365
+ ...baseLogAttributes,
366
+ "app.slack.retry_attempt": attempt
367
+ },
368
+ "Retrying Slack action after transient failure"
369
+ );
370
+ const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
371
+ const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
372
+ await sleep(retryAfterMs ?? backoffMs);
357
373
  }
358
374
  }
359
- return null;
375
+ throw new SlackActionError(
376
+ "Slack action exhausted retries",
377
+ "internal_error"
378
+ );
360
379
  }
361
- function loadSoul() {
362
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
363
- if (soul) {
364
- return soul;
365
- }
366
- logWarn(
367
- "soul_load_fallback",
368
- {},
369
- {
370
- "app.file.candidates": soulPathCandidates()
371
- },
372
- "SOUL.md not found; using built-in default personality"
373
- );
374
- return DEFAULT_SOUL;
380
+ function getSlackClient() {
381
+ return getClient();
375
382
  }
376
- function loadWorld() {
377
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
383
+ function isDmChannel(channelId) {
384
+ const normalized = normalizeSlackConversationId(channelId);
385
+ return Boolean(normalized && normalized.startsWith("D"));
378
386
  }
379
- var JUNIOR_PERSONALITY = (() => {
380
- try {
381
- return loadSoul();
382
- } catch (error) {
383
- logWarn(
384
- "soul_load_failed",
385
- {},
386
- {
387
- "exception.message": error instanceof Error ? error.message : String(error)
388
- },
389
- "Failed to load SOUL.md; using built-in default personality"
387
+ function isConversationScopedChannel(channelId) {
388
+ const normalized = normalizeSlackConversationId(channelId);
389
+ if (!normalized) return false;
390
+ return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
391
+ }
392
+ function isConversationChannel(channelId) {
393
+ const normalized = normalizeSlackConversationId(channelId);
394
+ if (!normalized) return false;
395
+ return normalized.startsWith("C") || normalized.startsWith("G");
396
+ }
397
+ async function getFilePermalink(fileId) {
398
+ const client2 = getClient();
399
+ const response = await withSlackRetries(
400
+ () => client2.files.info({
401
+ file: fileId
402
+ })
403
+ );
404
+ return response.file?.permalink;
405
+ }
406
+ async function downloadPrivateSlackFile(url) {
407
+ const token = getSlackBotToken();
408
+ if (!token) {
409
+ throw new SlackActionError(
410
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
411
+ "missing_token"
390
412
  );
391
- return DEFAULT_SOUL;
392
413
  }
393
- })();
394
- var JUNIOR_WORLD = (() => {
395
- try {
396
- return loadWorld();
397
- } catch (error) {
398
- logWarn(
399
- "world_load_failed",
400
- {},
401
- {
402
- "exception.message": error instanceof Error ? error.message : String(error)
403
- },
404
- "Failed to load WORLD.md; omitting world prompt context"
405
- );
406
- return null;
414
+ const response = await fetch(url, {
415
+ headers: {
416
+ Authorization: `Bearer ${token}`
417
+ }
418
+ });
419
+ if (!response.ok) {
420
+ throw new Error(`Slack file download failed: ${response.status}`);
407
421
  }
408
- })();
409
- function workspaceSkillDir(skillName) {
410
- return sandboxSkillDir(skillName);
422
+ return Buffer.from(await response.arrayBuffer());
411
423
  }
412
- function formatConfigurationValue(value) {
413
- if (typeof value === "string") {
414
- return escapeXml(value);
415
- }
416
- try {
417
- return escapeXml(JSON.stringify(value));
418
- } catch {
419
- return escapeXml(String(value));
420
- }
424
+
425
+ // src/chat/credentials/subject.ts
426
+ var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
427
+ var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
428
+ function getCredentialSubjectSecret() {
429
+ return process.env.JUNIOR_SECRET?.trim() || void 0;
421
430
  }
422
- function renderRequesterBlock(fields) {
423
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
424
- if (lines.length === 0) {
425
- return null;
431
+ function buildPayload(input) {
432
+ return [
433
+ CREDENTIAL_SUBJECT_HMAC_CONTEXT,
434
+ input.allowedWhen,
435
+ input.teamId,
436
+ input.channelId,
437
+ input.userId
438
+ ].join("\0");
439
+ }
440
+ function signPayload(secret, payload) {
441
+ const digest = createHmac("sha256", secret).update(payload).digest("hex");
442
+ return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
443
+ }
444
+ function timingSafeMatch(expected, actual) {
445
+ const expectedBuffer = Buffer.from(expected);
446
+ const actualBuffer = Buffer.from(actual);
447
+ if (expectedBuffer.length !== actualBuffer.length) {
448
+ return false;
449
+ }
450
+ return timingSafeEqual(expectedBuffer, actualBuffer);
451
+ }
452
+ function createSlackDirectCredentialSubject(input) {
453
+ const channelId = normalizeSlackConversationId(input.channelId);
454
+ const teamId = input.teamId?.trim();
455
+ const userId = parseActorUserId(input.userId);
456
+ if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
457
+ return void 0;
426
458
  }
427
- return ["<requester>", ...lines, "</requester>"];
428
- }
429
- function renderTag(tag, lines) {
430
- return [`<${tag}>`, ...lines, `</${tag}>`];
431
- }
432
- function renderTagBlock(tag, content) {
433
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
459
+ return {
460
+ type: "user",
461
+ userId,
462
+ allowedWhen: "private-direct-conversation"
463
+ };
434
464
  }
435
- function formatSkillEntry(skill) {
436
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
437
- const lines = [];
438
- lines.push(" <skill>");
439
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
440
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
441
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
442
- lines.push(" </skill>");
443
- return lines;
465
+ function bindSlackDirectCredentialSubject(input) {
466
+ const channelId = normalizeSlackConversationId(input.channelId);
467
+ const teamId = input.teamId.trim();
468
+ const secret = getCredentialSubjectSecret();
469
+ const { subject } = input;
470
+ const userId = parseActorUserId(subject.userId);
471
+ if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
472
+ return void 0;
473
+ }
474
+ return {
475
+ type: "user",
476
+ userId,
477
+ allowedWhen: subject.allowedWhen,
478
+ binding: {
479
+ type: "slack-direct-conversation",
480
+ teamId,
481
+ channelId,
482
+ signature: signPayload(
483
+ secret,
484
+ buildPayload({
485
+ allowedWhen: subject.allowedWhen,
486
+ teamId,
487
+ channelId,
488
+ userId
489
+ })
490
+ )
491
+ }
492
+ };
444
493
  }
445
- function formatAvailableSkillsForPrompt(skills, invocation) {
446
- const autoSelectable = skills.filter(
447
- (s) => s.disableModelInvocation !== true
494
+ function verifySlackDirectCredentialSubject(input) {
495
+ const channelId = normalizeSlackConversationId(input.channelId);
496
+ const secret = getCredentialSubjectSecret();
497
+ if (!channelId || !secret) {
498
+ return false;
499
+ }
500
+ const { subject } = input;
501
+ const binding = subject.binding;
502
+ if (subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) {
503
+ return false;
504
+ }
505
+ const expected = signPayload(
506
+ secret,
507
+ buildPayload({
508
+ allowedWhen: subject.allowedWhen,
509
+ teamId: binding.teamId,
510
+ channelId: binding.channelId,
511
+ userId: subject.userId
512
+ })
448
513
  );
449
- const invokedExplicitOnly = invocation ? skills.filter(
450
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
451
- ) : [];
452
- const sections = [];
453
- if (autoSelectable.length > 0) {
454
- const available = [
455
- "<available-skills>",
456
- "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
457
- ];
458
- for (const skill of autoSelectable) {
459
- available.push(...formatSkillEntry(skill));
460
- }
461
- available.push("</available-skills>");
462
- sections.push(available.join("\n"));
514
+ return timingSafeMatch(expected, binding.signature);
515
+ }
516
+
517
+ // src/chat/plugins/agent-hooks.ts
518
+ var AgentPluginHookDeniedError = class extends Error {
519
+ constructor(message) {
520
+ super(message);
521
+ this.name = "AgentPluginHookDeniedError";
463
522
  }
464
- if (invokedExplicitOnly.length > 0) {
465
- const userCallable = [
466
- "<user-callable-skills>",
467
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
468
- ];
469
- for (const skill of invokedExplicitOnly) {
470
- userCallable.push(...formatSkillEntry(skill));
523
+ };
524
+ var agentPlugins = [];
525
+ var AGENT_PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
526
+ var AGENT_PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
527
+ var OPERATIONAL_REPORT_MAX_METRICS = 8;
528
+ var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
529
+ var OPERATIONAL_REPORT_MAX_FIELDS = 8;
530
+ var OPERATIONAL_REPORT_MAX_RECORDS = 25;
531
+ var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
532
+ var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
533
+ var AGENT_PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
534
+ "GET",
535
+ "POST",
536
+ "PUT",
537
+ "PATCH",
538
+ "DELETE",
539
+ "HEAD",
540
+ "OPTIONS",
541
+ "ALL"
542
+ ]);
543
+ function validateLegacyStatePrefixes(plugin) {
544
+ const prefixes = plugin.legacyStatePrefixes;
545
+ if (prefixes === void 0) {
546
+ return;
547
+ }
548
+ if (!Array.isArray(prefixes)) {
549
+ throw new Error(
550
+ `Trusted plugin "${plugin.name}" legacyStatePrefixes must be an array`
551
+ );
552
+ }
553
+ const allowedPrefix = `junior:${plugin.name}`;
554
+ for (const rawPrefix of prefixes) {
555
+ const prefix = typeof rawPrefix === "string" ? rawPrefix.trim() : "";
556
+ if (!prefix) {
557
+ throw new Error(
558
+ `Trusted plugin "${plugin.name}" legacy state prefixes must be non-empty strings`
559
+ );
560
+ }
561
+ if (prefix !== allowedPrefix && !prefix.startsWith(`${allowedPrefix}:`)) {
562
+ throw new Error(
563
+ `Trusted plugin "${plugin.name}" legacy state prefix "${prefix}" must stay under "${allowedPrefix}"`
564
+ );
471
565
  }
472
- userCallable.push("</user-callable-skills>");
473
- sections.push(userCallable.join("\n"));
474
566
  }
475
- return sections.length > 0 ? sections.join("\n") : null;
476
567
  }
477
- function formatActiveMcpCatalogsForPrompt(catalogs) {
478
- if (catalogs.length === 0) {
479
- return null;
480
- }
481
- const lines = [
482
- "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
483
- ];
484
- for (const catalog of catalogs) {
485
- lines.push(" <catalog>");
486
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
487
- lines.push(
488
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
489
- );
490
- lines.push(" </catalog>");
568
+ function validateAgentPlugins(plugins) {
569
+ const seen = /* @__PURE__ */ new Set();
570
+ for (const plugin of plugins) {
571
+ if (!AGENT_PLUGIN_NAME_RE.test(plugin.name)) {
572
+ throw new Error(
573
+ `Trusted plugin name "${plugin.name}" must be a lowercase plugin identifier`
574
+ );
575
+ }
576
+ if (seen.has(plugin.name)) {
577
+ throw new Error(`Duplicate trusted plugin name "${plugin.name}"`);
578
+ }
579
+ seen.add(plugin.name);
580
+ validateLegacyStatePrefixes(plugin);
491
581
  }
492
- return lines.join("\n");
493
582
  }
494
- function formatToolGuidanceForPrompt(tools) {
495
- const guidedTools = tools.filter(
496
- (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
583
+ function setAgentPlugins(plugins) {
584
+ validateAgentPlugins(plugins);
585
+ const previous = agentPlugins;
586
+ agentPlugins = [...plugins].sort(
587
+ (left, right) => left.name.localeCompare(right.name)
497
588
  );
498
- if (guidedTools.length === 0) {
499
- return null;
500
- }
501
- const lines = [];
502
- for (const tool of guidedTools) {
503
- lines.push(` <tool name="${escapeXml(tool.name)}">`);
504
- if (tool.promptSnippet?.trim()) {
505
- lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
589
+ return previous;
590
+ }
591
+ function getAgentPlugins() {
592
+ return [...agentPlugins];
593
+ }
594
+ function getAgentPluginTools(context) {
595
+ const tools = {};
596
+ for (const plugin of getAgentPlugins()) {
597
+ const hook = plugin.hooks?.tools;
598
+ if (!hook) {
599
+ continue;
506
600
  }
507
- if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
508
- for (const guideline of tool.promptGuidelines) {
509
- lines.push(` - ${escapeXml(guideline)}`);
601
+ const log = createAgentPluginLogger(plugin.name);
602
+ const credentialSubject = createSlackDirectCredentialSubject({
603
+ channelId: context.channelId,
604
+ teamId: context.teamId,
605
+ userId: context.requester?.userId
606
+ });
607
+ const pluginTools = hook({
608
+ plugin: { name: plugin.name },
609
+ log,
610
+ requester: context.requester,
611
+ channelCapabilities: context.channelCapabilities,
612
+ channelId: context.channelId,
613
+ ...credentialSubject ? { credentialSubject } : {},
614
+ teamId: context.teamId,
615
+ messageTs: context.messageTs,
616
+ threadTs: context.threadTs,
617
+ userText: context.userText,
618
+ state: createPluginState(plugin.name, {
619
+ legacyStatePrefixes: plugin.legacyStatePrefixes
620
+ })
621
+ });
622
+ for (const [name, tool] of Object.entries(pluginTools)) {
623
+ if (!AGENT_PLUGIN_TOOL_NAME_RE.test(name)) {
624
+ throw new Error(
625
+ `Trusted plugin tool "${name}" from plugin "${plugin.name}" must be a camelCase identifier`
626
+ );
627
+ }
628
+ if (tools[name]) {
629
+ throw new Error(
630
+ `Duplicate trusted plugin tool "${name}" from plugin "${plugin.name}"`
631
+ );
510
632
  }
633
+ tools[name] = tool;
511
634
  }
512
- lines.push(" </tool>");
513
635
  }
514
- return lines.join("\n");
636
+ return tools;
515
637
  }
516
- function formatReferenceFilesLines() {
517
- const files = listReferenceFiles();
518
- if (files.length === 0) {
519
- return null;
638
+ function routeMethods(route, pluginName) {
639
+ const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
640
+ if (methods.length === 0) {
641
+ throw new Error(
642
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
643
+ );
520
644
  }
521
- return files.map((filePath) => {
522
- const name = path.basename(filePath);
523
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
524
- });
525
- }
526
- function formatArtifactsLines(artifactState) {
527
- if (!artifactState) return null;
528
- const lines = [];
529
- if (artifactState.lastCanvasId) {
530
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
645
+ for (const method of methods) {
646
+ if (!AGENT_PLUGIN_ROUTE_METHODS.has(method)) {
647
+ throw new Error(
648
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
649
+ );
650
+ }
531
651
  }
532
- if (artifactState.lastCanvasUrl) {
533
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
652
+ if (methods.includes("ALL") && methods.length > 1) {
653
+ throw new Error(
654
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
655
+ );
534
656
  }
535
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
536
- lines.push("- recent_canvases:");
537
- for (const canvas of artifactState.recentCanvases) {
538
- lines.push(` - id: ${escapeXml(canvas.id)}`);
539
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
540
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
541
- if (canvas.createdAt) {
542
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
657
+ return methods;
658
+ }
659
+ function getAgentPluginRoutes() {
660
+ const routes = [];
661
+ const seen = /* @__PURE__ */ new Set();
662
+ const methodsByPath = /* @__PURE__ */ new Map();
663
+ for (const plugin of getAgentPlugins()) {
664
+ const hook = plugin.hooks?.routes;
665
+ if (!hook) {
666
+ continue;
667
+ }
668
+ const log = createAgentPluginLogger(plugin.name);
669
+ const pluginRoutes = hook({
670
+ plugin: { name: plugin.name },
671
+ log
672
+ });
673
+ if (!Array.isArray(pluginRoutes)) {
674
+ throw new Error(
675
+ `Trusted plugin routes hook from plugin "${plugin.name}" must return an array`
676
+ );
677
+ }
678
+ for (const route of pluginRoutes) {
679
+ if (!isRecord2(route)) {
680
+ throw new Error(
681
+ `Trusted plugin route from plugin "${plugin.name}" must be an object`
682
+ );
543
683
  }
684
+ if (typeof route.path !== "string" || !route.path.startsWith("/")) {
685
+ throw new Error(
686
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must start with /`
687
+ );
688
+ }
689
+ if (typeof route.handler !== "function") {
690
+ throw new Error(
691
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must provide a handler`
692
+ );
693
+ }
694
+ const methods = routeMethods(route, plugin.name);
695
+ const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
696
+ if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
697
+ throw new Error(
698
+ `Trusted plugin route "${route.path}" conflicts with an ALL route for the same path`
699
+ );
700
+ }
701
+ for (const method of methods) {
702
+ const key2 = `${method}:${route.path}`;
703
+ if (seen.has(key2)) {
704
+ throw new Error(
705
+ `Duplicate trusted plugin route "${method} ${route.path}"`
706
+ );
707
+ }
708
+ seen.add(key2);
709
+ pathMethods.add(method);
710
+ }
711
+ methodsByPath.set(route.path, pathMethods);
712
+ routes.push({
713
+ ...route,
714
+ pluginName: plugin.name
715
+ });
544
716
  }
545
717
  }
546
- if (artifactState.lastListId) {
547
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
548
- }
549
- if (artifactState.lastListUrl) {
550
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
551
- }
552
- return lines.length > 0 ? lines : null;
718
+ return routes;
553
719
  }
554
- function formatConfigurationLines(configuration) {
555
- const keys = Object.keys(configuration ?? {}).sort(
556
- (a, b) => a.localeCompare(b)
557
- );
558
- if (keys.length === 0) return null;
559
- return keys.map(
560
- (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
561
- );
562
- }
563
- var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
564
- var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
565
- var TOOL_POLICY_RULES = [
566
- "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
567
- "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
568
- "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
569
- "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
570
- "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
571
- `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
572
- "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
573
- "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
574
- "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
575
- ];
576
- var TOOL_CALL_STYLE_RULES = [
577
- "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
578
- "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
579
- "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
580
- "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
581
- ];
582
- var SKILL_POLICY_RULES = [
583
- "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
584
- "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
585
- ];
586
- var EXECUTION_CONTRACT_RULES = [
587
- "- Actionable request: act in this turn.",
588
- "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
589
- "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
590
- "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
591
- "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
592
- "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
593
- ];
594
- var CONVERSATION_RULES = [
595
- "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
596
- "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
597
- "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
598
- ];
599
- var SLACK_ACTION_RULES = [
600
- "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
601
- "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
602
- "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
603
- "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
604
- "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
605
- "- Do not use reactions as progress indicators."
606
- ];
607
- var SAFETY_RULES = [
608
- "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
609
- "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
610
- "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
611
- ];
612
- var FAILURE_RULES = [
613
- "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
614
- "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
615
- "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
616
- ];
617
- function renderRuleSection(tag, lines) {
618
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
619
- }
620
- function buildBehaviorSection() {
621
- return [
622
- renderRuleSection("tool-policy", TOOL_POLICY_RULES),
623
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
624
- renderRuleSection("skill-policy", SKILL_POLICY_RULES),
625
- renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
626
- renderRuleSection("conversation", CONVERSATION_RULES),
627
- renderRuleSection("slack-actions", SLACK_ACTION_RULES),
628
- renderRuleSection("safety", SAFETY_RULES),
629
- renderRuleSection("failure-handling", FAILURE_RULES)
630
- ].join("\n\n");
631
- }
632
- function buildOutputSection() {
633
- const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
634
- return [
635
- openTag,
636
- "- Start with the answer or result, not internal process narration.",
637
- "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
638
- "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
639
- "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
640
- "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
641
- "</output>"
642
- ].join("\n");
720
+ function trustedSlackConversationUrl(pluginName, link) {
721
+ const url = typeof link?.url === "string" ? link.url.trim() : "";
722
+ if (!url) {
723
+ return void 0;
724
+ }
725
+ let parsed;
726
+ try {
727
+ parsed = new URL(url);
728
+ } catch (error) {
729
+ throw new Error(
730
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
731
+ { cause: error }
732
+ );
733
+ }
734
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
735
+ throw new Error(
736
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
737
+ );
738
+ }
739
+ return parsed.toString();
643
740
  }
644
- function buildIdentitySection() {
645
- return [
646
- "# Identity",
647
- `Your Slack username is \`${botConfig.userName}\`.`
648
- ].join("\n");
741
+ function getAgentPluginSlackConversationLink(conversationId) {
742
+ for (const plugin of getAgentPlugins()) {
743
+ const hook = plugin.hooks?.slackConversationLink;
744
+ if (!hook) {
745
+ continue;
746
+ }
747
+ const log = createAgentPluginLogger(plugin.name);
748
+ const link = hook({
749
+ plugin: { name: plugin.name },
750
+ log,
751
+ conversationId
752
+ });
753
+ const url = trustedSlackConversationUrl(plugin.name, link);
754
+ if (url) {
755
+ return { url };
756
+ }
757
+ }
758
+ return void 0;
649
759
  }
650
- function buildPersonalitySection() {
651
- return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
760
+ function pluginReadState(state) {
761
+ return {
762
+ get: state.get
763
+ };
652
764
  }
653
- function buildWorldSection() {
654
- if (!JUNIOR_WORLD) {
655
- return null;
765
+ function operationalReportText(value, maxLength) {
766
+ if (typeof value !== "string") {
767
+ return void 0;
656
768
  }
657
- return ["# World", JUNIOR_WORLD.trim()].join("\n");
658
- }
659
- function buildRuntimeSection(params) {
660
- const lines = [
661
- params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
662
- params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
663
- params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
664
- ].filter(Boolean);
665
- if (lines.length === 0) {
666
- return null;
769
+ const trimmed = value.trim();
770
+ if (!trimmed) {
771
+ return void 0;
667
772
  }
668
- return renderTagBlock("runtime", lines.join("\n"));
773
+ return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
669
774
  }
670
- function buildContextSection(params) {
671
- const blocks = [];
672
- const referenceLines = formatReferenceFilesLines();
673
- if (referenceLines) {
674
- blocks.push(
675
- renderTag("reference-files", [
676
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
677
- ...referenceLines
678
- ])
775
+ function operationalReportTone(tone) {
776
+ return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
777
+ }
778
+ function sanitizeOperationalReport(args) {
779
+ const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
780
+ const label = operationalReportText(
781
+ metric.label,
782
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
679
783
  );
680
- }
681
- const requesterLines = renderRequesterBlock({
682
- full_name: params.requester?.fullName,
683
- user_name: params.requester?.userName,
684
- user_id: params.requester?.userId
685
- });
686
- if (requesterLines) {
687
- blocks.push(requesterLines);
688
- }
689
- const artifactLines = formatArtifactsLines(params.artifactState);
690
- if (artifactLines) {
691
- blocks.push(renderTag("artifacts", artifactLines));
692
- }
693
- const configLines = formatConfigurationLines(params.configuration);
694
- if (configLines) {
695
- blocks.push(
696
- renderTag("configuration", [
697
- "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
698
- ...configLines
699
- ])
784
+ const value = operationalReportText(
785
+ metric.value,
786
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
700
787
  );
701
- }
702
- if (params.invocation) {
703
- blocks.push(
704
- renderTag("explicit-skill-trigger", [
705
- "Treat this skill as selected. Load it unless the tool says it is unavailable.",
706
- `/${escapeXml(params.invocation.skillName)}`
707
- ])
788
+ if (!label || !value) {
789
+ return void 0;
790
+ }
791
+ const sanitizedMetric = { label, value };
792
+ const tone = operationalReportTone(metric.tone);
793
+ if (tone) {
794
+ sanitizedMetric.tone = tone;
795
+ }
796
+ return sanitizedMetric;
797
+ }).filter((metric) => Boolean(metric));
798
+ const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
799
+ const title2 = operationalReportText(
800
+ recordSet.title,
801
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
802
+ );
803
+ if (!title2) {
804
+ return void 0;
805
+ }
806
+ const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
807
+ const key2 = operationalReportText(
808
+ field.key,
809
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
810
+ );
811
+ const label = operationalReportText(
812
+ field.label,
813
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
814
+ );
815
+ return key2 && label ? { key: key2, label } : void 0;
816
+ }).filter((field) => Boolean(field));
817
+ const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
818
+ const id = operationalReportText(
819
+ record.id,
820
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
821
+ ) ?? `${recordSetIndex}:${recordIndex}`;
822
+ const values = Object.fromEntries(
823
+ (fields ?? []).map((field) => [
824
+ field.key,
825
+ operationalReportText(
826
+ record.values[field.key],
827
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
828
+ ) ?? ""
829
+ ])
830
+ );
831
+ const sanitizedRecord = {
832
+ id,
833
+ values
834
+ };
835
+ const tone = operationalReportTone(record.tone);
836
+ if (tone) {
837
+ sanitizedRecord.tone = tone;
838
+ }
839
+ return sanitizedRecord;
840
+ });
841
+ const sanitizedRecordSet = { title: title2 };
842
+ if (fields?.length) {
843
+ sanitizedRecordSet.fields = fields;
844
+ }
845
+ const emptyText = operationalReportText(
846
+ recordSet.emptyText,
847
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
708
848
  );
849
+ if (emptyText) {
850
+ sanitizedRecordSet.emptyText = emptyText;
851
+ }
852
+ if (records?.length) {
853
+ sanitizedRecordSet.records = records;
854
+ }
855
+ return sanitizedRecordSet;
856
+ }).filter(
857
+ (recordSet) => Boolean(recordSet)
858
+ );
859
+ const sanitized = {
860
+ pluginName: args.pluginName
861
+ };
862
+ const generatedAt = operationalReportText(
863
+ args.report.generatedAt,
864
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
865
+ );
866
+ if (generatedAt) {
867
+ sanitized.generatedAt = generatedAt;
709
868
  }
710
- const body = blocks.map((block) => block.join("\n")).join("\n\n");
711
- if (!body) {
712
- return null;
869
+ if (recordSets?.length) {
870
+ sanitized.recordSets = recordSets;
713
871
  }
714
- return renderTagBlock("context", body);
715
- }
716
- function buildCapabilitiesSection(params) {
717
- const blocks = [];
718
- const availableSkills = formatAvailableSkillsForPrompt(
719
- params.availableSkills,
720
- params.invocation
721
- );
722
- if (availableSkills) {
723
- blocks.push(availableSkills);
872
+ if (metrics?.length) {
873
+ sanitized.metrics = metrics;
724
874
  }
725
- const activeCatalogs = formatActiveMcpCatalogsForPrompt(
726
- params.activeMcpCatalogs
875
+ const title = operationalReportText(
876
+ args.report.title,
877
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
727
878
  );
728
- if (activeCatalogs) {
729
- blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
730
- }
731
- const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
732
- if (toolGuidance) {
733
- blocks.push(renderTagBlock("tool-guidance", toolGuidance));
879
+ if (title) {
880
+ sanitized.title = title;
734
881
  }
735
- if (blocks.length === 0) {
736
- return null;
882
+ return sanitized;
883
+ }
884
+ function failedOperationalReport(args) {
885
+ return {
886
+ generatedAt: new Date(args.nowMs).toISOString(),
887
+ pluginName: args.pluginName,
888
+ metrics: [{ label: "report", tone: "danger", value: "failed" }],
889
+ title: args.pluginName,
890
+ recordSets: [
891
+ {
892
+ emptyText: "This plugin report failed to load.",
893
+ title: "Error"
894
+ }
895
+ ]
896
+ };
897
+ }
898
+ async function getAgentPluginOperationalReports(nowMs = Date.now()) {
899
+ const reports = [];
900
+ for (const plugin of getAgentPlugins()) {
901
+ const hook = plugin.hooks?.operationalReport;
902
+ if (!hook) {
903
+ continue;
904
+ }
905
+ const log = createAgentPluginLogger(plugin.name);
906
+ try {
907
+ const state = createPluginState(plugin.name, {
908
+ legacyStatePrefixes: plugin.legacyStatePrefixes
909
+ });
910
+ const report = await hook({
911
+ plugin: { name: plugin.name },
912
+ log,
913
+ nowMs,
914
+ state: pluginReadState(state)
915
+ });
916
+ if (!report) {
917
+ continue;
918
+ }
919
+ reports.push(
920
+ sanitizeOperationalReport({
921
+ pluginName: plugin.name,
922
+ report
923
+ })
924
+ );
925
+ } catch (error) {
926
+ log.error("Trusted plugin operational report failed", {
927
+ error: error instanceof Error ? error.message : String(error)
928
+ });
929
+ reports.push(failedOperationalReport({ nowMs, pluginName: plugin.name }));
930
+ }
737
931
  }
738
- return blocks.join("\n\n");
932
+ return reports;
739
933
  }
740
- var STATIC_SYSTEM_PROMPT = [
741
- HEADER,
742
- buildIdentitySection(),
743
- buildPersonalitySection(),
744
- buildWorldSection(),
745
- buildBehaviorSection(),
746
- buildOutputSection()
747
- ].filter((section) => Boolean(section)).join("\n\n");
748
- function buildSystemPrompt() {
749
- return STATIC_SYSTEM_PROMPT;
934
+ function isRecord2(value) {
935
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
750
936
  }
751
- function buildTurnContextPrompt(params) {
752
- const includeSessionContext = params.includeSessionContext ?? true;
753
- if (!includeSessionContext) {
754
- return null;
937
+ function normalizeEnv(value) {
938
+ if (!isRecord2(value)) {
939
+ return {};
755
940
  }
756
- const runtimeSections = [
757
- buildCapabilitiesSection({
758
- availableSkills: params.availableSkills,
759
- activeMcpCatalogs: params.activeMcpCatalogs ?? [],
760
- invocation: params.invocation,
761
- toolGuidance: params.toolGuidance ?? []
762
- }),
763
- buildContextSection({
764
- requester: params.requester,
765
- artifactState: params.artifactState,
766
- configuration: params.configuration,
767
- invocation: params.invocation
768
- }),
769
- buildRuntimeSection(params.runtime ?? {})
770
- ].filter((section) => Boolean(section));
771
- if (runtimeSections.length === 0) {
772
- return null;
941
+ const env = {};
942
+ for (const [key2, rawValue] of Object.entries(value)) {
943
+ if (typeof rawValue === "string") {
944
+ env[key2] = rawValue;
945
+ }
773
946
  }
774
- const sections = [
947
+ return env;
948
+ }
949
+ function createSandboxCapability(sandbox) {
950
+ return {
951
+ root: SANDBOX_WORKSPACE_ROOT,
952
+ juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
953
+ async readFile(filePath) {
954
+ return await sandbox.readFileToBuffer({ path: filePath }) ?? null;
955
+ },
956
+ async run(input) {
957
+ const result = await sandbox.runCommand(input);
958
+ const [stdout, stderr] = await Promise.all([
959
+ result.stdout(),
960
+ result.stderr()
961
+ ]);
962
+ return {
963
+ exitCode: result.exitCode,
964
+ stdout,
965
+ stderr
966
+ };
967
+ },
968
+ async writeFile(input) {
969
+ await sandbox.writeFiles([
970
+ {
971
+ path: input.path,
972
+ content: input.content,
973
+ ...input.mode !== void 0 ? { mode: input.mode } : {}
974
+ }
975
+ ]);
976
+ }
977
+ };
978
+ }
979
+ function createAgentPluginHookRunner(input = {}) {
980
+ const loaded = getAgentPlugins();
981
+ return {
982
+ async prepareSandbox(sandbox) {
983
+ const sandboxCapability = createSandboxCapability(sandbox);
984
+ for (const plugin of loaded) {
985
+ const hook = plugin.hooks?.sandboxPrepare;
986
+ if (!hook) {
987
+ continue;
988
+ }
989
+ logInfo(
990
+ "agent_plugin_hook_sandbox_prepare",
991
+ {},
992
+ { "app.plugin.name": plugin.name },
993
+ "Running agent plugin sandbox prepare hook"
994
+ );
995
+ await hook({
996
+ plugin: { name: plugin.name },
997
+ log: createAgentPluginLogger(plugin.name),
998
+ requester: input.requester,
999
+ sandbox: sandboxCapability
1000
+ });
1001
+ }
1002
+ },
1003
+ async beforeToolExecute(tool) {
1004
+ let nextInput = { ...tool.input };
1005
+ const env = normalizeEnv(nextInput.env);
1006
+ for (const plugin of loaded) {
1007
+ const hook = plugin.hooks?.beforeToolExecute;
1008
+ if (!hook) {
1009
+ continue;
1010
+ }
1011
+ let replacement;
1012
+ let denied;
1013
+ await hook({
1014
+ plugin: { name: plugin.name },
1015
+ log: createAgentPluginLogger(plugin.name),
1016
+ requester: input.requester,
1017
+ tool: {
1018
+ name: tool.name,
1019
+ input: nextInput
1020
+ },
1021
+ env: {
1022
+ get(key2) {
1023
+ return env[key2];
1024
+ },
1025
+ set(key2, value) {
1026
+ env[key2] = value;
1027
+ }
1028
+ },
1029
+ decision: {
1030
+ deny(message) {
1031
+ denied = message;
1032
+ },
1033
+ replaceInput(input2) {
1034
+ replacement = input2;
1035
+ }
1036
+ }
1037
+ });
1038
+ if (denied) {
1039
+ throw new AgentPluginHookDeniedError(denied);
1040
+ }
1041
+ if (replacement !== void 0) {
1042
+ if (!isRecord2(replacement)) {
1043
+ throw new Error(
1044
+ `Plugin "${plugin.name}" replaced tool input with a non-object value`
1045
+ );
1046
+ }
1047
+ nextInput = { ...replacement };
1048
+ Object.assign(env, normalizeEnv(nextInput.env));
1049
+ }
1050
+ }
1051
+ return {
1052
+ input: {
1053
+ ...nextInput,
1054
+ ...Object.keys(env).length > 0 ? { env } : {}
1055
+ },
1056
+ env
1057
+ };
1058
+ }
1059
+ };
1060
+ }
1061
+
1062
+ // src/handlers/health.ts
1063
+ function GET() {
1064
+ return Response.json({
1065
+ status: "ok",
1066
+ service: "junior",
1067
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1068
+ });
1069
+ }
1070
+
1071
+ // src/chat/prompt.ts
1072
+ import fs from "fs";
1073
+ import path from "path";
1074
+
1075
+ // src/chat/turn-context-tag.ts
1076
+ var TURN_CONTEXT_TAG = "runtime-turn-context";
1077
+
1078
+ // src/chat/interruption-marker.ts
1079
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
1080
+ function getInterruptionMarker() {
1081
+ return INTERRUPTED_MARKER;
1082
+ }
1083
+
1084
+ // src/chat/slack/status-format.ts
1085
+ var SLACK_STATUS_MAX_LENGTH = 50;
1086
+ function truncateStatusText(text) {
1087
+ const trimmed = text.trim();
1088
+ if (!trimmed) {
1089
+ return "";
1090
+ }
1091
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
1092
+ return trimmed;
1093
+ }
1094
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
1095
+ }
1096
+
1097
+ // src/chat/slack/mrkdwn.ts
1098
+ function ensureBlockSpacing(text) {
1099
+ const codeBlockPattern = /^```/;
1100
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
1101
+ const lines = text.split("\n");
1102
+ const result = [];
1103
+ let inCodeBlock = false;
1104
+ for (let i = 0; i < lines.length; i++) {
1105
+ const line = lines[i];
1106
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
1107
+ if (isCodeFence) {
1108
+ if (!inCodeBlock) {
1109
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
1110
+ if (prev2 !== void 0 && prev2.trim() !== "") {
1111
+ result.push("");
1112
+ }
1113
+ }
1114
+ inCodeBlock = !inCodeBlock;
1115
+ result.push(line);
1116
+ continue;
1117
+ }
1118
+ if (inCodeBlock) {
1119
+ result.push(line);
1120
+ continue;
1121
+ }
1122
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
1123
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
1124
+ result.push("");
1125
+ }
1126
+ result.push(line);
1127
+ }
1128
+ return result.join("\n");
1129
+ }
1130
+ function renderSlackMrkdwn(text) {
1131
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1132
+ normalized = ensureBlockSpacing(normalized);
1133
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
1134
+ }
1135
+ function normalizeSlackStatusText(text) {
1136
+ const trimmed = text.trim();
1137
+ if (!trimmed) {
1138
+ return "";
1139
+ }
1140
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
1141
+ }
1142
+
1143
+ // src/chat/slack/output.ts
1144
+ var MAX_INLINE_CHARS = 2200;
1145
+ var MAX_INLINE_LINES = 45;
1146
+ var CONTINUED_MARKER = "\n\n[Continued below]";
1147
+ function countSlackLines(text) {
1148
+ if (!text) {
1149
+ return 0;
1150
+ }
1151
+ return text.split("\n").length;
1152
+ }
1153
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1154
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
1155
+ }
1156
+ function findSplitIndex(text, maxChars) {
1157
+ if (text.length <= maxChars) {
1158
+ return text.length;
1159
+ }
1160
+ const bounded = text.slice(0, maxChars);
1161
+ const newlineIndex = bounded.lastIndexOf("\n");
1162
+ if (newlineIndex > 0) {
1163
+ return newlineIndex;
1164
+ }
1165
+ const spaceIndex = bounded.lastIndexOf(" ");
1166
+ if (spaceIndex > 0) {
1167
+ return spaceIndex;
1168
+ }
1169
+ return maxChars;
1170
+ }
1171
+ function splitByLineBudget(text, maxLines) {
1172
+ if (maxLines <= 0) {
1173
+ return "";
1174
+ }
1175
+ const lines = text.split("\n");
1176
+ if (lines.length <= maxLines) {
1177
+ return text;
1178
+ }
1179
+ return lines.slice(0, maxLines).join("\n");
1180
+ }
1181
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1182
+ return {
1183
+ maxChars: Math.max(1, maxChars - suffix.length),
1184
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1185
+ };
1186
+ }
1187
+ function forceSplitBudget(text, budget) {
1188
+ const lineCount = countSlackLines(text);
1189
+ return {
1190
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1191
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1192
+ };
1193
+ }
1194
+ function getFenceContinuation(text) {
1195
+ let open;
1196
+ for (const line of text.split("\n")) {
1197
+ const trimmed = line.trimStart();
1198
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1199
+ if (!openerMatch) {
1200
+ continue;
1201
+ }
1202
+ if (!open) {
1203
+ open = {
1204
+ fence: openerMatch[1],
1205
+ openerLine: trimmed
1206
+ };
1207
+ continue;
1208
+ }
1209
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1210
+ open = void 0;
1211
+ }
1212
+ }
1213
+ if (!open) {
1214
+ return null;
1215
+ }
1216
+ return {
1217
+ closeSuffix: text.endsWith("\n") ? open.fence : `
1218
+ ${open.fence}`,
1219
+ reopenPrefix: `${open.openerLine}
1220
+ `
1221
+ };
1222
+ }
1223
+ function appendSlackSuffix(text, marker) {
1224
+ const carryover = getFenceContinuation(text);
1225
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1226
+ }
1227
+ function stripTrailingContinuationMarker(text) {
1228
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1229
+ }
1230
+ function takeSlackContinuationChunk(text, budget) {
1231
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1232
+ if (!rest) {
1233
+ ({ prefix, rest } = takeSlackInlinePrefix(
1234
+ text,
1235
+ forceSplitBudget(text, budget)
1236
+ ));
1237
+ }
1238
+ let carryover = rest ? getFenceContinuation(prefix) : null;
1239
+ if (!carryover) {
1240
+ return { prefix, rest };
1241
+ }
1242
+ const carryoverBudget = reserveInlineBudgetForSuffix(
1243
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
1244
+ );
1245
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1246
+ if (!rest) {
1247
+ ({ prefix, rest } = takeSlackInlinePrefix(
1248
+ text,
1249
+ forceSplitBudget(text, carryoverBudget)
1250
+ ));
1251
+ }
1252
+ carryover = rest ? getFenceContinuation(prefix) : null;
1253
+ if (!carryover) {
1254
+ return { prefix, rest };
1255
+ }
1256
+ return {
1257
+ prefix,
1258
+ rest: `${carryover.reopenPrefix}${rest}`
1259
+ };
1260
+ }
1261
+ function takeSlackContinuationPrefix(text, options) {
1262
+ const budget = {
1263
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1264
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1265
+ };
1266
+ const { prefix, rest } = (() => {
1267
+ if (options?.forceSplit) {
1268
+ return takeSlackContinuationChunk(text, budget);
1269
+ }
1270
+ const initial = takeSlackInlinePrefix(text, budget);
1271
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1272
+ })();
1273
+ return {
1274
+ prefix,
1275
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1276
+ rest
1277
+ };
1278
+ }
1279
+ function takeSlackInlinePrefix(text, options) {
1280
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1281
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1282
+ const normalized = text.replace(/\r\n?/g, "\n");
1283
+ if (!normalized) {
1284
+ return { prefix: "", rest: "" };
1285
+ }
1286
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1287
+ return { prefix: normalized, rest: "" };
1288
+ }
1289
+ const lineBounded = splitByLineBudget(normalized, maxLines);
1290
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
1291
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1292
+ if (prefix) {
1293
+ return {
1294
+ prefix,
1295
+ rest: normalized.slice(prefix.length).trimStart()
1296
+ };
1297
+ }
1298
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1299
+ return {
1300
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1301
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1302
+ };
1303
+ }
1304
+ function splitSlackReplyText(text, options) {
1305
+ const normalized = renderSlackMrkdwn(text);
1306
+ if (!normalized) {
1307
+ return [];
1308
+ }
1309
+ const chunks = [];
1310
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1311
+ let remaining = normalized;
1312
+ while (remaining) {
1313
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1314
+ if (fitsFinalChunk) {
1315
+ chunks.push(
1316
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1317
+ );
1318
+ break;
1319
+ }
1320
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1321
+ ...continuationBudget,
1322
+ forceSplit: true
1323
+ });
1324
+ chunks.push(renderedPrefix);
1325
+ remaining = rest;
1326
+ }
1327
+ if (chunks.length === 2) {
1328
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1329
+ }
1330
+ return chunks;
1331
+ }
1332
+ function getSlackContinuationBudget() {
1333
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1334
+ }
1335
+ function buildSlackOutputMessage(text, files) {
1336
+ const normalized = renderSlackMrkdwn(text);
1337
+ const fileCount = files?.length ?? 0;
1338
+ if (!normalized) {
1339
+ if (fileCount > 0) {
1340
+ return {
1341
+ raw: "",
1342
+ files
1343
+ };
1344
+ }
1345
+ throw new Error(
1346
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1347
+ );
1348
+ }
1349
+ return {
1350
+ markdown: normalized,
1351
+ files
1352
+ };
1353
+ }
1354
+ var slackOutputPolicy = {
1355
+ maxInlineChars: MAX_INLINE_CHARS,
1356
+ maxInlineLines: MAX_INLINE_LINES
1357
+ };
1358
+
1359
+ // src/chat/xml.ts
1360
+ function escapeXml(value) {
1361
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1362
+ }
1363
+
1364
+ // src/chat/prompt.ts
1365
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1366
+ function getLoggedMarkdownFiles() {
1367
+ const globalState = globalThis;
1368
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
1369
+ return globalState.__juniorLoggedMarkdownFiles;
1370
+ }
1371
+ function loadOptionalMarkdownFile(candidates, fileName) {
1372
+ for (const resolved of candidates) {
1373
+ try {
1374
+ const raw = fs.readFileSync(resolved, "utf8").trim();
1375
+ if (raw.length > 0) {
1376
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
1377
+ const logKey = `${fileName}:${resolved}`;
1378
+ if (!loggedMarkdownFiles.has(logKey)) {
1379
+ loggedMarkdownFiles.add(logKey);
1380
+ logInfo(
1381
+ `${fileName.toLowerCase()}_loaded`,
1382
+ {},
1383
+ {
1384
+ "file.path": resolved
1385
+ },
1386
+ `Loaded ${fileName}`
1387
+ );
1388
+ }
1389
+ return raw;
1390
+ }
1391
+ } catch {
1392
+ continue;
1393
+ }
1394
+ }
1395
+ return null;
1396
+ }
1397
+ function loadSoul() {
1398
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1399
+ if (soul) {
1400
+ return soul;
1401
+ }
1402
+ logWarn(
1403
+ "soul_load_fallback",
1404
+ {},
1405
+ {
1406
+ "app.file.candidates": soulPathCandidates()
1407
+ },
1408
+ "SOUL.md not found; using built-in default personality"
1409
+ );
1410
+ return DEFAULT_SOUL;
1411
+ }
1412
+ function loadWorld() {
1413
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
1414
+ }
1415
+ var JUNIOR_PERSONALITY = (() => {
1416
+ try {
1417
+ return loadSoul();
1418
+ } catch (error) {
1419
+ logWarn(
1420
+ "soul_load_failed",
1421
+ {},
1422
+ {
1423
+ "exception.message": error instanceof Error ? error.message : String(error)
1424
+ },
1425
+ "Failed to load SOUL.md; using built-in default personality"
1426
+ );
1427
+ return DEFAULT_SOUL;
1428
+ }
1429
+ })();
1430
+ var JUNIOR_WORLD = (() => {
1431
+ try {
1432
+ return loadWorld();
1433
+ } catch (error) {
1434
+ logWarn(
1435
+ "world_load_failed",
1436
+ {},
1437
+ {
1438
+ "exception.message": error instanceof Error ? error.message : String(error)
1439
+ },
1440
+ "Failed to load WORLD.md; omitting world prompt context"
1441
+ );
1442
+ return null;
1443
+ }
1444
+ })();
1445
+ function workspaceSkillDir(skillName) {
1446
+ return sandboxSkillDir(skillName);
1447
+ }
1448
+ function formatConfigurationValue(value) {
1449
+ if (typeof value === "string") {
1450
+ return escapeXml(value);
1451
+ }
1452
+ try {
1453
+ return escapeXml(JSON.stringify(value));
1454
+ } catch {
1455
+ return escapeXml(String(value));
1456
+ }
1457
+ }
1458
+ function renderRequesterBlock(fields) {
1459
+ const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
1460
+ if (lines.length === 0) {
1461
+ return null;
1462
+ }
1463
+ return ["<requester>", ...lines, "</requester>"];
1464
+ }
1465
+ function renderTag(tag, lines) {
1466
+ return [`<${tag}>`, ...lines, `</${tag}>`];
1467
+ }
1468
+ function renderTagBlock(tag, content) {
1469
+ return [`<${tag}>`, content, `</${tag}>`].join("\n");
1470
+ }
1471
+ function formatSkillEntry(skill) {
1472
+ const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
1473
+ const lines = [];
1474
+ lines.push(" <skill>");
1475
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
1476
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
1477
+ lines.push(` <location>${escapeXml(skillLocation)}</location>`);
1478
+ lines.push(" </skill>");
1479
+ return lines;
1480
+ }
1481
+ function formatAvailableSkillsForPrompt(skills, invocation) {
1482
+ const autoSelectable = skills.filter(
1483
+ (s) => s.disableModelInvocation !== true
1484
+ );
1485
+ const invokedExplicitOnly = invocation ? skills.filter(
1486
+ (s) => s.disableModelInvocation === true && s.name === invocation.skillName
1487
+ ) : [];
1488
+ const sections = [];
1489
+ if (autoSelectable.length > 0) {
1490
+ const available = [
1491
+ "<available-skills>",
1492
+ "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
1493
+ ];
1494
+ for (const skill of autoSelectable) {
1495
+ available.push(...formatSkillEntry(skill));
1496
+ }
1497
+ available.push("</available-skills>");
1498
+ sections.push(available.join("\n"));
1499
+ }
1500
+ if (invokedExplicitOnly.length > 0) {
1501
+ const userCallable = [
1502
+ "<user-callable-skills>",
1503
+ "The user's current message explicitly references this skill by name. Load it when relevant to the request."
1504
+ ];
1505
+ for (const skill of invokedExplicitOnly) {
1506
+ userCallable.push(...formatSkillEntry(skill));
1507
+ }
1508
+ userCallable.push("</user-callable-skills>");
1509
+ sections.push(userCallable.join("\n"));
1510
+ }
1511
+ return sections.length > 0 ? sections.join("\n") : null;
1512
+ }
1513
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
1514
+ if (catalogs.length === 0) {
1515
+ return null;
1516
+ }
1517
+ const lines = [
1518
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
1519
+ ];
1520
+ for (const catalog of catalogs) {
1521
+ lines.push(" <catalog>");
1522
+ lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
1523
+ lines.push(
1524
+ ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
1525
+ );
1526
+ lines.push(" </catalog>");
1527
+ }
1528
+ return lines.join("\n");
1529
+ }
1530
+ function formatToolGuidanceForPrompt(tools) {
1531
+ const guidedTools = tools.filter(
1532
+ (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
1533
+ );
1534
+ if (guidedTools.length === 0) {
1535
+ return null;
1536
+ }
1537
+ const lines = [];
1538
+ for (const tool of guidedTools) {
1539
+ lines.push(` <tool name="${escapeXml(tool.name)}">`);
1540
+ if (tool.promptSnippet?.trim()) {
1541
+ lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
1542
+ }
1543
+ if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1544
+ for (const guideline of tool.promptGuidelines) {
1545
+ lines.push(` - ${escapeXml(guideline)}`);
1546
+ }
1547
+ }
1548
+ lines.push(" </tool>");
1549
+ }
1550
+ return lines.join("\n");
1551
+ }
1552
+ function formatReferenceFilesLines() {
1553
+ const files = listReferenceFiles();
1554
+ if (files.length === 0) {
1555
+ return null;
1556
+ }
1557
+ return files.map((filePath) => {
1558
+ const name = path.basename(filePath);
1559
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1560
+ });
1561
+ }
1562
+ function formatArtifactsLines(artifactState) {
1563
+ if (!artifactState) return null;
1564
+ const lines = [];
1565
+ if (artifactState.lastCanvasId) {
1566
+ lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
1567
+ }
1568
+ if (artifactState.lastCanvasUrl) {
1569
+ lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1570
+ }
1571
+ if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1572
+ lines.push("- recent_canvases:");
1573
+ for (const canvas of artifactState.recentCanvases) {
1574
+ lines.push(` - id: ${escapeXml(canvas.id)}`);
1575
+ if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1576
+ if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1577
+ if (canvas.createdAt) {
1578
+ lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1579
+ }
1580
+ }
1581
+ }
1582
+ if (artifactState.lastListId) {
1583
+ lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1584
+ }
1585
+ if (artifactState.lastListUrl) {
1586
+ lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
1587
+ }
1588
+ return lines.length > 0 ? lines : null;
1589
+ }
1590
+ function formatConfigurationLines(configuration) {
1591
+ const keys = Object.keys(configuration ?? {}).sort(
1592
+ (a, b) => a.localeCompare(b)
1593
+ );
1594
+ if (keys.length === 0) return null;
1595
+ return keys.map(
1596
+ (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
1597
+ );
1598
+ }
1599
+ var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
1600
+ var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1601
+ var TOOL_POLICY_RULES = [
1602
+ "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
1603
+ "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
1604
+ "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1605
+ "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
1606
+ "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
1607
+ `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
1608
+ "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
1609
+ "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1610
+ "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1611
+ ];
1612
+ var TOOL_CALL_STYLE_RULES = [
1613
+ "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1614
+ "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1615
+ "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
1616
+ "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1617
+ ];
1618
+ var SKILL_POLICY_RULES = [
1619
+ "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1620
+ "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1621
+ ];
1622
+ var EXECUTION_CONTRACT_RULES = [
1623
+ "- Actionable request: act in this turn.",
1624
+ "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
1625
+ "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
1626
+ "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1627
+ "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1628
+ "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
1629
+ ];
1630
+ var CONVERSATION_RULES = [
1631
+ "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1632
+ "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1633
+ "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1634
+ ];
1635
+ var SLACK_ACTION_RULES = [
1636
+ "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1637
+ "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1638
+ "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1639
+ "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
1640
+ "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
1641
+ "- Do not use reactions as progress indicators."
1642
+ ];
1643
+ var SAFETY_RULES = [
1644
+ "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1645
+ "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1646
+ "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
1647
+ ];
1648
+ var FAILURE_RULES = [
1649
+ "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1650
+ "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
1651
+ "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
1652
+ ];
1653
+ function renderRuleSection(tag, lines) {
1654
+ return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
1655
+ }
1656
+ function buildBehaviorSection() {
1657
+ return [
1658
+ renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1659
+ renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1660
+ renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1661
+ renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
1662
+ renderRuleSection("conversation", CONVERSATION_RULES),
1663
+ renderRuleSection("slack-actions", SLACK_ACTION_RULES),
1664
+ renderRuleSection("safety", SAFETY_RULES),
1665
+ renderRuleSection("failure-handling", FAILURE_RULES)
1666
+ ].join("\n\n");
1667
+ }
1668
+ function buildOutputSection() {
1669
+ const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
1670
+ return [
1671
+ openTag,
1672
+ "- Start with the answer or result, not internal process narration.",
1673
+ "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
1674
+ "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
1675
+ "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
1676
+ "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
1677
+ "</output>"
1678
+ ].join("\n");
1679
+ }
1680
+ function buildIdentitySection() {
1681
+ return [
1682
+ "# Identity",
1683
+ `Your Slack username is \`${botConfig.userName}\`.`
1684
+ ].join("\n");
1685
+ }
1686
+ function buildPersonalitySection() {
1687
+ return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
1688
+ }
1689
+ function buildWorldSection() {
1690
+ if (!JUNIOR_WORLD) {
1691
+ return null;
1692
+ }
1693
+ return ["# World", JUNIOR_WORLD.trim()].join("\n");
1694
+ }
1695
+ function buildRuntimeSection(params) {
1696
+ const lines = [
1697
+ params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
1698
+ params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
1699
+ params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
1700
+ ].filter(Boolean);
1701
+ if (lines.length === 0) {
1702
+ return null;
1703
+ }
1704
+ return renderTagBlock("runtime", lines.join("\n"));
1705
+ }
1706
+ function buildContextSection(params) {
1707
+ const blocks = [];
1708
+ const referenceLines = formatReferenceFilesLines();
1709
+ if (referenceLines) {
1710
+ blocks.push(
1711
+ renderTag("reference-files", [
1712
+ "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
1713
+ ...referenceLines
1714
+ ])
1715
+ );
1716
+ }
1717
+ const requesterLines = renderRequesterBlock({
1718
+ full_name: params.requester?.fullName,
1719
+ user_name: params.requester?.userName,
1720
+ user_id: params.requester?.userId
1721
+ });
1722
+ if (requesterLines) {
1723
+ blocks.push(requesterLines);
1724
+ }
1725
+ const artifactLines = formatArtifactsLines(params.artifactState);
1726
+ if (artifactLines) {
1727
+ blocks.push(renderTag("artifacts", artifactLines));
1728
+ }
1729
+ const configLines = formatConfigurationLines(params.configuration);
1730
+ if (configLines) {
1731
+ blocks.push(
1732
+ renderTag("configuration", [
1733
+ "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
1734
+ ...configLines
1735
+ ])
1736
+ );
1737
+ }
1738
+ if (params.invocation) {
1739
+ blocks.push(
1740
+ renderTag("explicit-skill-trigger", [
1741
+ "Treat this skill as selected. Load it unless the tool says it is unavailable.",
1742
+ `/${escapeXml(params.invocation.skillName)}`
1743
+ ])
1744
+ );
1745
+ }
1746
+ const body = blocks.map((block) => block.join("\n")).join("\n\n");
1747
+ if (!body) {
1748
+ return null;
1749
+ }
1750
+ return renderTagBlock("context", body);
1751
+ }
1752
+ function buildCapabilitiesSection(params) {
1753
+ const blocks = [];
1754
+ const availableSkills = formatAvailableSkillsForPrompt(
1755
+ params.availableSkills,
1756
+ params.invocation
1757
+ );
1758
+ if (availableSkills) {
1759
+ blocks.push(availableSkills);
1760
+ }
1761
+ const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1762
+ params.activeMcpCatalogs
1763
+ );
1764
+ if (activeCatalogs) {
1765
+ blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
1766
+ }
1767
+ const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
1768
+ if (toolGuidance) {
1769
+ blocks.push(renderTagBlock("tool-guidance", toolGuidance));
1770
+ }
1771
+ if (blocks.length === 0) {
1772
+ return null;
1773
+ }
1774
+ return blocks.join("\n\n");
1775
+ }
1776
+ var STATIC_SYSTEM_PROMPT = [
1777
+ HEADER,
1778
+ buildIdentitySection(),
1779
+ buildPersonalitySection(),
1780
+ buildWorldSection(),
1781
+ buildBehaviorSection(),
1782
+ buildOutputSection()
1783
+ ].filter((section) => Boolean(section)).join("\n\n");
1784
+ function buildSystemPrompt() {
1785
+ return STATIC_SYSTEM_PROMPT;
1786
+ }
1787
+ function buildTurnContextPrompt(params) {
1788
+ const includeSessionContext = params.includeSessionContext ?? true;
1789
+ if (!includeSessionContext) {
1790
+ return null;
1791
+ }
1792
+ const runtimeSections = [
1793
+ buildCapabilitiesSection({
1794
+ availableSkills: params.availableSkills,
1795
+ activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1796
+ invocation: params.invocation,
1797
+ toolGuidance: params.toolGuidance ?? []
1798
+ }),
1799
+ buildContextSection({
1800
+ requester: params.requester,
1801
+ artifactState: params.artifactState,
1802
+ configuration: params.configuration,
1803
+ invocation: params.invocation
1804
+ }),
1805
+ buildRuntimeSection(params.runtime ?? {})
1806
+ ].filter((section) => Boolean(section));
1807
+ if (runtimeSections.length === 0) {
1808
+ return null;
1809
+ }
1810
+ const sections = [
775
1811
  `<${TURN_CONTEXT_TAG}>`,
776
1812
  TURN_CONTEXT_HEADER,
777
1813
  "The current user instruction appears after this block in the same message.",
@@ -1243,6 +2279,9 @@ function parseAgentTurnSessionStatus(parsed) {
1243
2279
  }
1244
2280
  return void 0;
1245
2281
  }
2282
+ function parseAgentTurnSurface(value) {
2283
+ return value === "slack" || value === "api" || value === "scheduler" || value === "internal" ? value : void 0;
2284
+ }
1246
2285
  function parseAgentTurnSessionFields(parsed) {
1247
2286
  const status = parseAgentTurnSessionStatus(parsed);
1248
2287
  if (!status) {
@@ -1261,6 +2300,7 @@ function parseAgentTurnSessionFields(parsed) {
1261
2300
  const logSessionId = typeof parsed.logSessionId === "string" ? parsed.logSessionId : void 0;
1262
2301
  const requester = parseAgentTurnRequester(parsed.requester);
1263
2302
  const startedAtMs = toFiniteNonNegativeNumber(parsed.startedAtMs);
2303
+ const surface = parseAgentTurnSurface(parsed.surface);
1264
2304
  if (typeof conversationId !== "string" || typeof sessionId !== "string" || sliceId === void 0 || version === void 0 || updatedAtMs === void 0) {
1265
2305
  return void 0;
1266
2306
  }
@@ -1287,6 +2327,7 @@ function parseAgentTurnSessionFields(parsed) {
1287
2327
  ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" || parsed.resumeReason === "yield" ? { resumeReason: parsed.resumeReason } : {},
1288
2328
  ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
1289
2329
  ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {},
2330
+ ...surface ? { surface } : {},
1290
2331
  ...typeof parsed.traceId === "string" ? { traceId: parsed.traceId } : {}
1291
2332
  };
1292
2333
  }
@@ -1372,6 +2413,7 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
1372
2413
  ...stored.loadedSkillNames ? { loadedSkillNames: stored.loadedSkillNames } : {},
1373
2414
  ...stored.requester ? { requester: stored.requester } : {},
1374
2415
  ...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
2416
+ ...stored.surface ? { surface: stored.surface } : {},
1375
2417
  ...stored.traceId ? { traceId: stored.traceId } : {}
1376
2418
  };
1377
2419
  }
@@ -1437,6 +2479,7 @@ function buildStoredRecord(args) {
1437
2479
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1438
2480
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1439
2481
  ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2482
+ ...args.surface ? { surface: args.surface } : {},
1440
2483
  ...args.traceId ? { traceId: args.traceId } : {}
1441
2484
  };
1442
2485
  }
@@ -1486,6 +2529,7 @@ async function updateAgentTurnSessionState(args) {
1486
2529
  ...args.existing.requester ? { requester: args.existing.requester } : {},
1487
2530
  ...args.existing.resumeReason ? { resumeReason: args.existing.resumeReason } : {},
1488
2531
  ...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
2532
+ ...args.existing.surface ? { surface: args.existing.surface } : {},
1489
2533
  ...args.existing.traceId ? { traceId: args.existing.traceId } : {},
1490
2534
  ...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
1491
2535
  })
@@ -1526,6 +2570,7 @@ async function upsertAgentTurnSessionRecord(args) {
1526
2570
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1527
2571
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1528
2572
  ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2573
+ ...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
1529
2574
  ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
1530
2575
  })
1531
2576
  });
@@ -1560,6 +2605,7 @@ async function recordAgentTurnSessionSummary(args) {
1560
2605
  )
1561
2606
  } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
1562
2607
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
2608
+ ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
1563
2609
  ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
1564
2610
  },
1565
2611
  ttlMs
@@ -1573,380 +2619,111 @@ async function readAgentTurnSessionSummariesFromIndex(key2) {
1573
2619
  for (const value of [...values].reverse()) {
1574
2620
  const summary = parseAgentTurnSessionSummary(value);
1575
2621
  if (!summary) {
1576
- continue;
1577
- }
1578
- const key3 = `${summary.conversationId}:${summary.sessionId}`;
1579
- if (!summaries.has(key3)) {
1580
- summaries.set(key3, summary);
1581
- }
1582
- }
1583
- return [...summaries.values()].sort(
1584
- (left, right) => right.updatedAtMs - left.updatedAtMs
1585
- );
1586
- }
1587
- async function listAgentTurnSessionSummaries(limit = 50) {
1588
- return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
1589
- }
1590
- async function listAgentTurnSessionSummariesForConversation(conversationId) {
1591
- const summaries = await readAgentTurnSessionSummariesFromIndex(
1592
- agentTurnSessionConversationIndexKey(conversationId)
1593
- );
1594
- if (summaries.length > 0) {
1595
- return summaries;
1596
- }
1597
- return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
1598
- }
1599
- async function abandonAgentTurnSessionRecord(args) {
1600
- const existing = await getAgentTurnSessionRecord(
1601
- args.conversationId,
1602
- args.sessionId
1603
- );
1604
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
1605
- return void 0;
1606
- }
1607
- return await updateAgentTurnSessionState({
1608
- existing,
1609
- state: "abandoned",
1610
- errorMessage: args.errorMessage ?? existing.errorMessage
1611
- });
1612
- }
1613
- async function failAgentTurnSessionRecord(args) {
1614
- const existing = await getAgentTurnSessionRecord(
1615
- args.conversationId,
1616
- args.sessionId
1617
- );
1618
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
1619
- return void 0;
1620
- }
1621
- return await updateAgentTurnSessionState({
1622
- existing,
1623
- state: "failed",
1624
- errorMessage: args.errorMessage ?? existing.errorMessage
1625
- });
1626
- }
1627
-
1628
- // src/chat/sentry-links.ts
1629
- function getSentryOrgSlug() {
1630
- const slug = process.env.SENTRY_ORG_SLUG?.trim();
1631
- return slug || void 0;
1632
- }
1633
- function isSentrySaasDsnHost(host) {
1634
- return host === "sentry.io" || host.endsWith(".sentry.io");
1635
- }
1636
- function buildSentryWebBaseUrl(dsn) {
1637
- if (isSentrySaasDsnHost(dsn.host)) {
1638
- return "https://sentry.io";
1639
- }
1640
- const port = dsn.port ? `:${dsn.port}` : "";
1641
- const path2 = dsn.path ? `/${dsn.path}` : "";
1642
- return `${dsn.protocol}://${dsn.host}${port}${path2}`;
1643
- }
1644
- function buildSentryConversationUrl(conversationId) {
1645
- const client2 = sentry_exports.getClient();
1646
- const dsn = client2?.getDsn();
1647
- if (!dsn?.host || !dsn.projectId) {
1648
- return void 0;
1649
- }
1650
- const orgSlug = getSentryOrgSlug();
1651
- if (!orgSlug) {
1652
- return void 0;
1653
- }
1654
- const encodedId = encodeURIComponent(conversationId);
1655
- const params = new URLSearchParams();
1656
- params.set("project", dsn.projectId);
1657
- const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
1658
- if (isSentrySaasDsnHost(dsn.host)) {
1659
- return `https://${orgSlug}.sentry.io/${path2}`;
1660
- }
1661
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1662
- }
1663
- function buildSentryTraceUrl(traceId) {
1664
- const client2 = sentry_exports.getClient();
1665
- const dsn = client2?.getDsn();
1666
- if (!dsn?.host || !dsn.projectId) {
1667
- return void 0;
1668
- }
1669
- const orgSlug = getSentryOrgSlug();
1670
- if (!orgSlug) {
1671
- return void 0;
1672
- }
1673
- const encodedTraceId = encodeURIComponent(traceId);
1674
- const params = new URLSearchParams();
1675
- params.set("project", dsn.projectId);
1676
- const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
1677
- if (isSentrySaasDsnHost(dsn.host)) {
1678
- return `https://${orgSlug}.sentry.io/${path2}`;
1679
- }
1680
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1681
- }
1682
-
1683
- // src/chat/slack/client.ts
1684
- import { WebClient } from "@slack/web-api";
1685
- var SlackActionError = class extends Error {
1686
- code;
1687
- apiError;
1688
- needed;
1689
- provided;
1690
- statusCode;
1691
- requestId;
1692
- errorData;
1693
- retryAfterSeconds;
1694
- detail;
1695
- detailLine;
1696
- detailRule;
1697
- constructor(message, code, options = {}) {
1698
- super(message);
1699
- this.name = "SlackActionError";
1700
- this.code = code;
1701
- this.apiError = options.apiError;
1702
- this.needed = options.needed;
1703
- this.provided = options.provided;
1704
- this.statusCode = options.statusCode;
1705
- this.requestId = options.requestId;
1706
- this.errorData = options.errorData;
1707
- this.retryAfterSeconds = options.retryAfterSeconds;
1708
- this.detail = options.detail;
1709
- this.detailLine = options.detailLine;
1710
- this.detailRule = options.detailRule;
1711
- }
1712
- };
1713
- function serializeSlackErrorData(data) {
1714
- if (!data || typeof data !== "object") {
1715
- return void 0;
1716
- }
1717
- const filtered = Object.fromEntries(
1718
- Object.entries(data).filter(
1719
- ([key2]) => key2 !== "error"
1720
- )
1721
- );
1722
- if (Object.keys(filtered).length === 0) {
1723
- return void 0;
1724
- }
1725
- try {
1726
- const serialized = JSON.stringify(filtered);
1727
- return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
1728
- } catch {
1729
- return void 0;
1730
- }
1731
- }
1732
- function getHeaderString(headers, name) {
1733
- if (!headers || typeof headers !== "object") {
1734
- return void 0;
1735
- }
1736
- const key2 = name.toLowerCase();
1737
- const entries = headers;
1738
- for (const [entryKey, value] of Object.entries(entries)) {
1739
- if (entryKey.toLowerCase() !== key2) continue;
1740
- if (typeof value === "string") return value;
1741
- if (Array.isArray(value)) {
1742
- const first = value.find((entry) => typeof entry === "string");
1743
- return typeof first === "string" ? first : void 0;
1744
- }
1745
- }
1746
- return void 0;
1747
- }
1748
- function parseSlackCanvasDetail(detail) {
1749
- if (typeof detail !== "string") {
1750
- return {};
1751
- }
1752
- const trimmed = detail.trim();
1753
- if (!trimmed) {
1754
- return {};
1755
- }
1756
- const parsed = {
1757
- detail: trimmed
1758
- };
1759
- const lineMatch = trimmed.match(/line\s+(\d+):/i);
1760
- if (lineMatch) {
1761
- const line = Number.parseInt(lineMatch[1] ?? "", 10);
1762
- if (Number.isFinite(line)) {
1763
- parsed.detailLine = line;
2622
+ continue;
2623
+ }
2624
+ const key3 = `${summary.conversationId}:${summary.sessionId}`;
2625
+ if (!summaries.has(key3)) {
2626
+ summaries.set(key3, summary);
1764
2627
  }
1765
2628
  }
1766
- if (/unsupported heading depth/i.test(trimmed)) {
1767
- parsed.detailRule = "unsupported_heading_depth";
1768
- }
1769
- return parsed;
2629
+ return [...summaries.values()].sort(
2630
+ (left, right) => right.updatedAtMs - left.updatedAtMs
2631
+ );
1770
2632
  }
1771
- var client = null;
1772
- function normalizeSlackConversationId(channelId) {
1773
- if (!channelId) return void 0;
1774
- const trimmed = channelId.trim();
1775
- if (!trimmed) return void 0;
1776
- if (!trimmed.startsWith("slack:")) {
1777
- return trimmed;
1778
- }
1779
- const parts = trimmed.split(":");
1780
- return parts[1]?.trim() || void 0;
2633
+ async function listAgentTurnSessionSummaries(limit = 50) {
2634
+ return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
1781
2635
  }
1782
- function getClient2() {
1783
- if (client) return client;
1784
- const token = getSlackBotToken();
1785
- if (!token) {
1786
- throw new SlackActionError(
1787
- "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
1788
- "missing_token"
1789
- );
2636
+ async function listAgentTurnSessionSummariesForConversation(conversationId) {
2637
+ const summaries = await readAgentTurnSessionSummariesFromIndex(
2638
+ agentTurnSessionConversationIndexKey(conversationId)
2639
+ );
2640
+ if (summaries.length > 0) {
2641
+ return summaries;
1790
2642
  }
1791
- client = new WebClient(token);
1792
- return client;
2643
+ return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
1793
2644
  }
1794
- function mapSlackError(error) {
1795
- if (error instanceof SlackActionError) {
1796
- return error;
1797
- }
1798
- const candidate = error;
1799
- const apiError = candidate.data?.error;
1800
- const message = candidate.message ?? "Slack action failed";
1801
- const baseOptions = {
1802
- apiError,
1803
- statusCode: candidate.statusCode,
1804
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
1805
- errorData: serializeSlackErrorData(candidate.data),
1806
- ...parseSlackCanvasDetail(candidate.data?.detail)
1807
- };
1808
- if (apiError === "missing_scope") {
1809
- return new SlackActionError(message, "missing_scope", {
1810
- ...baseOptions,
1811
- needed: candidate.data?.needed,
1812
- provided: candidate.data?.provided
1813
- });
1814
- }
1815
- if (apiError === "not_in_channel") {
1816
- return new SlackActionError(message, "not_in_channel", baseOptions);
1817
- }
1818
- if (apiError === "already_reacted") {
1819
- return new SlackActionError(message, "already_reacted", baseOptions);
1820
- }
1821
- if (apiError === "no_reaction") {
1822
- return new SlackActionError(message, "no_reaction", baseOptions);
1823
- }
1824
- if (apiError === "invalid_arguments") {
1825
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1826
- }
1827
- if (apiError === "invalid_cursor") {
1828
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1829
- }
1830
- if (apiError === "invalid_name") {
1831
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1832
- }
1833
- if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
1834
- return new SlackActionError(message, "not_found", baseOptions);
1835
- }
1836
- if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
1837
- return new SlackActionError(message, "feature_unavailable", baseOptions);
1838
- }
1839
- if (apiError === "canvas_creation_failed") {
1840
- return new SlackActionError(message, "canvas_creation_failed", baseOptions);
1841
- }
1842
- if (apiError === "canvas_editing_failed") {
1843
- return new SlackActionError(message, "canvas_editing_failed", baseOptions);
1844
- }
1845
- if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
1846
- return new SlackActionError(message, "rate_limited", {
1847
- ...baseOptions,
1848
- retryAfterSeconds: candidate.retryAfter
1849
- });
2645
+ async function abandonAgentTurnSessionRecord(args) {
2646
+ const existing = await getAgentTurnSessionRecord(
2647
+ args.conversationId,
2648
+ args.sessionId
2649
+ );
2650
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
2651
+ return void 0;
1850
2652
  }
1851
- return new SlackActionError(message, "internal_error", baseOptions);
1852
- }
1853
- function sleep(ms) {
1854
- return new Promise((resolve) => setTimeout(resolve, ms));
2653
+ return await updateAgentTurnSessionState({
2654
+ existing,
2655
+ state: "abandoned",
2656
+ errorMessage: args.errorMessage ?? existing.errorMessage
2657
+ });
1855
2658
  }
1856
- async function withSlackRetries(task, maxAttempts = 3, context = {}) {
1857
- let attempt = 0;
1858
- while (attempt < maxAttempts) {
1859
- attempt += 1;
1860
- try {
1861
- return await task();
1862
- } catch (error) {
1863
- const mapped = mapSlackError(error);
1864
- const isRetryable = mapped.code === "rate_limited";
1865
- const baseLogAttributes = {
1866
- "app.slack.action": context.action ?? "unknown",
1867
- "app.slack.error_code": mapped.code,
1868
- ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
1869
- ...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
1870
- ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
1871
- ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
1872
- ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
1873
- ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
1874
- ...context.attributes ?? {}
1875
- };
1876
- if (!isRetryable || attempt >= maxAttempts) {
1877
- logWarn(
1878
- "slack_action_failed",
1879
- {},
1880
- {
1881
- ...baseLogAttributes,
1882
- ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
1883
- },
1884
- "Slack action failed"
1885
- );
1886
- throw mapped;
1887
- }
1888
- logWarn(
1889
- "slack_action_retrying",
1890
- {},
1891
- {
1892
- ...baseLogAttributes,
1893
- "app.slack.retry_attempt": attempt
1894
- },
1895
- "Retrying Slack action after transient failure"
1896
- );
1897
- const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
1898
- const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
1899
- await sleep(retryAfterMs ?? backoffMs);
1900
- }
1901
- }
1902
- throw new SlackActionError(
1903
- "Slack action exhausted retries",
1904
- "internal_error"
2659
+ async function failAgentTurnSessionRecord(args) {
2660
+ const existing = await getAgentTurnSessionRecord(
2661
+ args.conversationId,
2662
+ args.sessionId
1905
2663
  );
2664
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
2665
+ return void 0;
2666
+ }
2667
+ return await updateAgentTurnSessionState({
2668
+ existing,
2669
+ state: "failed",
2670
+ errorMessage: args.errorMessage ?? existing.errorMessage
2671
+ });
1906
2672
  }
1907
- function getSlackClient() {
1908
- return getClient2();
1909
- }
1910
- function isDmChannel(channelId) {
1911
- const normalized = normalizeSlackConversationId(channelId);
1912
- return Boolean(normalized && normalized.startsWith("D"));
2673
+
2674
+ // src/chat/sentry-links.ts
2675
+ function getSentryOrgSlug() {
2676
+ const slug = process.env.SENTRY_ORG_SLUG?.trim();
2677
+ return slug || void 0;
1913
2678
  }
1914
- function isConversationScopedChannel(channelId) {
1915
- const normalized = normalizeSlackConversationId(channelId);
1916
- if (!normalized) return false;
1917
- return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
2679
+ function isSentrySaasDsnHost(host) {
2680
+ return host === "sentry.io" || host.endsWith(".sentry.io");
1918
2681
  }
1919
- function isConversationChannel(channelId) {
1920
- const normalized = normalizeSlackConversationId(channelId);
1921
- if (!normalized) return false;
1922
- return normalized.startsWith("C") || normalized.startsWith("G");
2682
+ function buildSentryWebBaseUrl(dsn) {
2683
+ if (isSentrySaasDsnHost(dsn.host)) {
2684
+ return "https://sentry.io";
2685
+ }
2686
+ const port = dsn.port ? `:${dsn.port}` : "";
2687
+ const path2 = dsn.path ? `/${dsn.path}` : "";
2688
+ return `${dsn.protocol}://${dsn.host}${port}${path2}`;
1923
2689
  }
1924
- async function getFilePermalink(fileId) {
1925
- const client2 = getClient2();
1926
- const response = await withSlackRetries(
1927
- () => client2.files.info({
1928
- file: fileId
1929
- })
1930
- );
1931
- return response.file?.permalink;
2690
+ function buildSentryConversationUrl(conversationId) {
2691
+ const client2 = sentry_exports.getClient();
2692
+ const dsn = client2?.getDsn();
2693
+ if (!dsn?.host || !dsn.projectId) {
2694
+ return void 0;
2695
+ }
2696
+ const orgSlug = getSentryOrgSlug();
2697
+ if (!orgSlug) {
2698
+ return void 0;
2699
+ }
2700
+ const encodedId = encodeURIComponent(conversationId);
2701
+ const params = new URLSearchParams();
2702
+ params.set("project", dsn.projectId);
2703
+ const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
2704
+ if (isSentrySaasDsnHost(dsn.host)) {
2705
+ return `https://${orgSlug}.sentry.io/${path2}`;
2706
+ }
2707
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1932
2708
  }
1933
- async function downloadPrivateSlackFile(url) {
1934
- const token = getSlackBotToken();
1935
- if (!token) {
1936
- throw new SlackActionError(
1937
- "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
1938
- "missing_token"
1939
- );
2709
+ function buildSentryTraceUrl(traceId) {
2710
+ const client2 = sentry_exports.getClient();
2711
+ const dsn = client2?.getDsn();
2712
+ if (!dsn?.host || !dsn.projectId) {
2713
+ return void 0;
1940
2714
  }
1941
- const response = await fetch(url, {
1942
- headers: {
1943
- Authorization: `Bearer ${token}`
1944
- }
1945
- });
1946
- if (!response.ok) {
1947
- throw new Error(`Slack file download failed: ${response.status}`);
2715
+ const orgSlug = getSentryOrgSlug();
2716
+ if (!orgSlug) {
2717
+ return void 0;
1948
2718
  }
1949
- return Buffer.from(await response.arrayBuffer());
2719
+ const encodedTraceId = encodeURIComponent(traceId);
2720
+ const params = new URLSearchParams();
2721
+ params.set("project", dsn.projectId);
2722
+ const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
2723
+ if (isSentrySaasDsnHost(dsn.host)) {
2724
+ return `https://${orgSlug}.sentry.io/${path2}`;
2725
+ }
2726
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1950
2727
  }
1951
2728
 
1952
2729
  // src/chat/slack/conversation-context.ts
@@ -2018,6 +2795,8 @@ function formatSlackConversationRedactedLabel(context) {
2018
2795
  }
2019
2796
 
2020
2797
  export {
2798
+ createAgentPluginLogger,
2799
+ createPluginState,
2021
2800
  SlackActionError,
2022
2801
  getHeaderString,
2023
2802
  normalizeSlackConversationId,
@@ -2028,6 +2807,16 @@ export {
2028
2807
  isConversationChannel,
2029
2808
  getFilePermalink,
2030
2809
  downloadPrivateSlackFile,
2810
+ bindSlackDirectCredentialSubject,
2811
+ verifySlackDirectCredentialSubject,
2812
+ validateAgentPlugins,
2813
+ setAgentPlugins,
2814
+ getAgentPlugins,
2815
+ getAgentPluginTools,
2816
+ getAgentPluginRoutes,
2817
+ getAgentPluginSlackConversationLink,
2818
+ getAgentPluginOperationalReports,
2819
+ createAgentPluginHookRunner,
2031
2820
  GET,
2032
2821
  TURN_CONTEXT_TAG,
2033
2822
  getInterruptionMarker,