@sentry/junior 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1886 -593
- package/dist/{chunk-NRSP2MLC.js → chunk-JWBWBJYJ.js} +9 -4
- package/dist/{chunk-N4ICA2BC.js → chunk-MCJJKEB3.js} +523 -95
- package/dist/{chunk-Z43DS7XN.js → chunk-THPM7NSG.js} +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +5 -4
package/dist/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadSkillsByName,
|
|
7
7
|
logCapabilityCatalogLoadedOnce,
|
|
8
8
|
parseSkillInvocation
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JWBWBJYJ.js";
|
|
10
10
|
import {
|
|
11
11
|
SANDBOX_DATA_ROOT,
|
|
12
12
|
SANDBOX_SKILLS_ROOT,
|
|
@@ -27,10 +27,11 @@ import {
|
|
|
27
27
|
sandboxSkillDir,
|
|
28
28
|
sandboxSkillFile,
|
|
29
29
|
toOptionalTrimmed
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-THPM7NSG.js";
|
|
31
31
|
import {
|
|
32
32
|
CredentialUnavailableError,
|
|
33
33
|
buildOAuthTokenRequest,
|
|
34
|
+
createChatSdkLogger,
|
|
34
35
|
createPluginBroker,
|
|
35
36
|
createRequestContext,
|
|
36
37
|
extractGenAiUsageAttributes,
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
toOptionalString,
|
|
58
59
|
withContext,
|
|
59
60
|
withSpan
|
|
60
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-MCJJKEB3.js";
|
|
61
62
|
import "./chunk-Z3YD6NHK.js";
|
|
62
63
|
import {
|
|
63
64
|
discoverInstalledPluginPackageContent,
|
|
@@ -310,9 +311,6 @@ async function GET3() {
|
|
|
310
311
|
});
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
// src/handlers/mcp-oauth-callback.ts
|
|
314
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
315
|
-
|
|
316
314
|
// src/chat/state/conversation.ts
|
|
317
315
|
function coerceRole(value) {
|
|
318
316
|
return value === "assistant" || value === "system" || value === "user" ? value : "user";
|
|
@@ -728,81 +726,6 @@ async function deleteMcpServerSessionId(userId, provider) {
|
|
|
728
726
|
await stateAdapter.delete(serverSessionKey(userId, provider));
|
|
729
727
|
}
|
|
730
728
|
|
|
731
|
-
// src/chat/slack/output.ts
|
|
732
|
-
var MAX_INLINE_CHARS = 2200;
|
|
733
|
-
var MAX_INLINE_LINES = 45;
|
|
734
|
-
function ensureBlockSpacing(text) {
|
|
735
|
-
const codeBlockPattern = /^```/;
|
|
736
|
-
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
737
|
-
const lines = text.split("\n");
|
|
738
|
-
const result = [];
|
|
739
|
-
let inCodeBlock = false;
|
|
740
|
-
for (let i = 0; i < lines.length; i++) {
|
|
741
|
-
const line = lines[i];
|
|
742
|
-
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
743
|
-
if (isCodeFence) {
|
|
744
|
-
if (!inCodeBlock) {
|
|
745
|
-
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
746
|
-
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
747
|
-
result.push("");
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
inCodeBlock = !inCodeBlock;
|
|
751
|
-
result.push(line);
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
if (inCodeBlock) {
|
|
755
|
-
result.push(line);
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
759
|
-
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
760
|
-
result.push("");
|
|
761
|
-
}
|
|
762
|
-
result.push(line);
|
|
763
|
-
}
|
|
764
|
-
return result.join("\n");
|
|
765
|
-
}
|
|
766
|
-
function normalizeForSlack(text) {
|
|
767
|
-
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
768
|
-
normalized = ensureBlockSpacing(normalized);
|
|
769
|
-
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
770
|
-
}
|
|
771
|
-
function buildSlackOutputMessage(text, files) {
|
|
772
|
-
const normalized = normalizeForSlack(text);
|
|
773
|
-
const fileCount = files?.length ?? 0;
|
|
774
|
-
if (!normalized) {
|
|
775
|
-
if (fileCount > 0) {
|
|
776
|
-
return {
|
|
777
|
-
raw: "",
|
|
778
|
-
files
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
logWarn(
|
|
782
|
-
"slack_output_normalized_empty",
|
|
783
|
-
{},
|
|
784
|
-
{
|
|
785
|
-
"app.output.original_length": text.length,
|
|
786
|
-
"app.output.parsed_length": normalized.length,
|
|
787
|
-
"app.output.file_count": fileCount
|
|
788
|
-
},
|
|
789
|
-
"Slack output normalized to empty content"
|
|
790
|
-
);
|
|
791
|
-
return {
|
|
792
|
-
markdown: "I couldn't produce a response.",
|
|
793
|
-
files
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
markdown: normalized,
|
|
798
|
-
files
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
var slackOutputPolicy = {
|
|
802
|
-
maxInlineChars: MAX_INLINE_CHARS,
|
|
803
|
-
maxInlineLines: MAX_INLINE_LINES
|
|
804
|
-
};
|
|
805
|
-
|
|
806
729
|
// src/chat/mcp/oauth.ts
|
|
807
730
|
import { randomUUID } from "crypto";
|
|
808
731
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -1786,7 +1709,7 @@ function validateConfigKey(key) {
|
|
|
1786
1709
|
return "Configuration key must not be empty";
|
|
1787
1710
|
}
|
|
1788
1711
|
if (!CONFIG_KEY_RE.test(trimmed)) {
|
|
1789
|
-
return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "
|
|
1712
|
+
return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "provider.repo")`;
|
|
1790
1713
|
}
|
|
1791
1714
|
if (SECRET_KEY_RE.test(trimmed)) {
|
|
1792
1715
|
return `Configuration key "${key}" appears to be secret-related and is not allowed`;
|
|
@@ -1808,7 +1731,9 @@ function collectStringValues(value, output, depth = 0) {
|
|
|
1808
1731
|
return;
|
|
1809
1732
|
}
|
|
1810
1733
|
if (value && typeof value === "object") {
|
|
1811
|
-
for (const [key, nested] of Object.entries(
|
|
1734
|
+
for (const [key, nested] of Object.entries(
|
|
1735
|
+
value
|
|
1736
|
+
)) {
|
|
1812
1737
|
output.push(key);
|
|
1813
1738
|
collectStringValues(nested, output, depth + 1);
|
|
1814
1739
|
}
|
|
@@ -2016,6 +1941,9 @@ function buildArtifactStatePatch(patch) {
|
|
|
2016
1941
|
function threadStateKey(threadId) {
|
|
2017
1942
|
return `thread-state:${threadId}`;
|
|
2018
1943
|
}
|
|
1944
|
+
function channelStateKey(channelId) {
|
|
1945
|
+
return `channel-state:${channelId}`;
|
|
1946
|
+
}
|
|
2019
1947
|
function buildThreadStatePayload(patch) {
|
|
2020
1948
|
const payload = {};
|
|
2021
1949
|
if (patch.artifacts) {
|
|
@@ -2045,6 +1973,14 @@ function mergeArtifactsState(artifacts, patch) {
|
|
|
2045
1973
|
}
|
|
2046
1974
|
};
|
|
2047
1975
|
}
|
|
1976
|
+
function getPersistedSandboxState(state) {
|
|
1977
|
+
return {
|
|
1978
|
+
sandboxId: toOptionalString(state.app_sandbox_id),
|
|
1979
|
+
sandboxDependencyProfileHash: toOptionalString(
|
|
1980
|
+
state.app_sandbox_dependency_profile_hash
|
|
1981
|
+
)
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
2048
1984
|
async function persistThreadState(thread, patch) {
|
|
2049
1985
|
const payload = buildThreadStatePayload(patch);
|
|
2050
1986
|
if (Object.keys(payload).length === 0) {
|
|
@@ -2059,6 +1995,13 @@ async function getPersistedThreadState(threadId) {
|
|
|
2059
1995
|
threadStateKey(threadId)
|
|
2060
1996
|
) ?? {};
|
|
2061
1997
|
}
|
|
1998
|
+
async function getPersistedChannelState(channelId) {
|
|
1999
|
+
const stateAdapter = getStateAdapter();
|
|
2000
|
+
await stateAdapter.connect();
|
|
2001
|
+
return await stateAdapter.get(
|
|
2002
|
+
channelStateKey(channelId)
|
|
2003
|
+
) ?? {};
|
|
2004
|
+
}
|
|
2062
2005
|
async function persistThreadStateById(threadId, patch) {
|
|
2063
2006
|
const payload = buildThreadStatePayload(patch);
|
|
2064
2007
|
if (Object.keys(payload).length === 0) {
|
|
@@ -2081,6 +2024,97 @@ function getChannelConfigurationService(thread) {
|
|
|
2081
2024
|
}
|
|
2082
2025
|
});
|
|
2083
2026
|
}
|
|
2027
|
+
function getChannelConfigurationServiceById(channelId) {
|
|
2028
|
+
return createChannelConfigurationService({
|
|
2029
|
+
load: async () => await getPersistedChannelState(channelId),
|
|
2030
|
+
save: async (state) => {
|
|
2031
|
+
const stateAdapter = getStateAdapter();
|
|
2032
|
+
await stateAdapter.connect();
|
|
2033
|
+
const key = channelStateKey(channelId);
|
|
2034
|
+
const existing = await stateAdapter.get(key) ?? {};
|
|
2035
|
+
await stateAdapter.set(
|
|
2036
|
+
key,
|
|
2037
|
+
{ ...existing, configuration: state },
|
|
2038
|
+
THREAD_STATE_TTL_MS
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/chat/runtime/thread-participants.ts
|
|
2045
|
+
function buildThreadParticipants(messages) {
|
|
2046
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2047
|
+
const participants = [];
|
|
2048
|
+
for (const message of messages) {
|
|
2049
|
+
const { userId, userName, fullName } = message.author ?? {};
|
|
2050
|
+
if (!userId || message.author?.isBot) continue;
|
|
2051
|
+
if (seen.has(userId)) continue;
|
|
2052
|
+
seen.add(userId);
|
|
2053
|
+
participants.push({ userId, userName, fullName });
|
|
2054
|
+
}
|
|
2055
|
+
return participants;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/chat/runtime/turn.ts
|
|
2059
|
+
function buildDeterministicTurnId(messageId) {
|
|
2060
|
+
const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2061
|
+
return `turn_${sanitized}`;
|
|
2062
|
+
}
|
|
2063
|
+
var RetryableTurnError = class extends Error {
|
|
2064
|
+
code = "retryable_turn";
|
|
2065
|
+
metadata;
|
|
2066
|
+
reason;
|
|
2067
|
+
constructor(reason, message, metadata) {
|
|
2068
|
+
super(message);
|
|
2069
|
+
this.name = "RetryableTurnError";
|
|
2070
|
+
this.reason = reason;
|
|
2071
|
+
this.metadata = metadata;
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
function isRetryableTurnError(error, reason) {
|
|
2075
|
+
if (!(error instanceof RetryableTurnError)) {
|
|
2076
|
+
return false;
|
|
2077
|
+
}
|
|
2078
|
+
if (!reason) {
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
return error.reason === reason;
|
|
2082
|
+
}
|
|
2083
|
+
function startActiveTurn(args) {
|
|
2084
|
+
args.conversation.processing.activeTurnId = args.nextTurnId;
|
|
2085
|
+
args.updateConversationStats(args.conversation);
|
|
2086
|
+
}
|
|
2087
|
+
function markTurnCompleted(args) {
|
|
2088
|
+
args.conversation.processing.activeTurnId = void 0;
|
|
2089
|
+
args.conversation.processing.lastCompletedAtMs = args.nowMs;
|
|
2090
|
+
args.updateConversationStats(args.conversation);
|
|
2091
|
+
}
|
|
2092
|
+
function markTurnFailed(args) {
|
|
2093
|
+
args.conversation.processing.activeTurnId = void 0;
|
|
2094
|
+
args.conversation.processing.lastCompletedAtMs = args.nowMs;
|
|
2095
|
+
args.markConversationMessage(args.conversation, args.userMessageId, {
|
|
2096
|
+
replied: false,
|
|
2097
|
+
skippedReason: "reply failed"
|
|
2098
|
+
});
|
|
2099
|
+
args.updateConversationStats(args.conversation);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/chat/runtime/turn-user-message.ts
|
|
2103
|
+
function getTurnUserMessage(conversation, sessionId) {
|
|
2104
|
+
for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
|
|
2105
|
+
const message = conversation.messages[index];
|
|
2106
|
+
if (message?.role !== "user") {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
if (buildDeterministicTurnId(message.id) === sessionId) {
|
|
2110
|
+
return message;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return void 0;
|
|
2114
|
+
}
|
|
2115
|
+
function getTurnUserMessageId(conversation, sessionId) {
|
|
2116
|
+
return getTurnUserMessage(conversation, sessionId)?.id;
|
|
2117
|
+
}
|
|
2084
2118
|
|
|
2085
2119
|
// src/chat/pi/client.ts
|
|
2086
2120
|
import {
|
|
@@ -2669,80 +2703,350 @@ import { Agent } from "@mariozechner/pi-agent-core";
|
|
|
2669
2703
|
// src/chat/prompt.ts
|
|
2670
2704
|
import fs from "fs";
|
|
2671
2705
|
import path2 from "path";
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
`Loaded ${fileName}`
|
|
2694
|
-
);
|
|
2706
|
+
|
|
2707
|
+
// src/chat/slack/output.ts
|
|
2708
|
+
var MAX_INLINE_CHARS = 2200;
|
|
2709
|
+
var MAX_INLINE_LINES = 45;
|
|
2710
|
+
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
2711
|
+
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
2712
|
+
var STREAMING_FENCE_CLOSE_GUARD = "\n```";
|
|
2713
|
+
function ensureBlockSpacing(text) {
|
|
2714
|
+
const codeBlockPattern = /^```/;
|
|
2715
|
+
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
2716
|
+
const lines = text.split("\n");
|
|
2717
|
+
const result = [];
|
|
2718
|
+
let inCodeBlock = false;
|
|
2719
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2720
|
+
const line = lines[i];
|
|
2721
|
+
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
2722
|
+
if (isCodeFence) {
|
|
2723
|
+
if (!inCodeBlock) {
|
|
2724
|
+
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
2725
|
+
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
2726
|
+
result.push("");
|
|
2695
2727
|
}
|
|
2696
|
-
return raw;
|
|
2697
2728
|
}
|
|
2698
|
-
|
|
2729
|
+
inCodeBlock = !inCodeBlock;
|
|
2730
|
+
result.push(line);
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
if (inCodeBlock) {
|
|
2734
|
+
result.push(line);
|
|
2699
2735
|
continue;
|
|
2700
2736
|
}
|
|
2737
|
+
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
2738
|
+
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
2739
|
+
result.push("");
|
|
2740
|
+
}
|
|
2741
|
+
result.push(line);
|
|
2701
2742
|
}
|
|
2702
|
-
return
|
|
2743
|
+
return result.join("\n");
|
|
2703
2744
|
}
|
|
2704
|
-
function
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2745
|
+
function normalizeForSlack(text) {
|
|
2746
|
+
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
2747
|
+
normalized = ensureBlockSpacing(normalized);
|
|
2748
|
+
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
2749
|
+
}
|
|
2750
|
+
function countSlackLines(text) {
|
|
2751
|
+
if (!text) {
|
|
2752
|
+
return 0;
|
|
2708
2753
|
}
|
|
2709
|
-
|
|
2710
|
-
"soul_load_fallback",
|
|
2711
|
-
{},
|
|
2712
|
-
{
|
|
2713
|
-
"file.candidates": soulPathCandidates()
|
|
2714
|
-
},
|
|
2715
|
-
"SOUL.md not found; using built-in default personality"
|
|
2716
|
-
);
|
|
2717
|
-
return DEFAULT_SOUL;
|
|
2754
|
+
return text.split("\n").length;
|
|
2718
2755
|
}
|
|
2719
|
-
function
|
|
2720
|
-
return
|
|
2756
|
+
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
2757
|
+
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
2721
2758
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
return
|
|
2725
|
-
} catch (error) {
|
|
2726
|
-
logWarn(
|
|
2727
|
-
"soul_load_failed",
|
|
2728
|
-
{},
|
|
2729
|
-
{
|
|
2730
|
-
"error.message": error instanceof Error ? error.message : String(error)
|
|
2731
|
-
},
|
|
2732
|
-
"Failed to load SOUL.md; using built-in default personality"
|
|
2733
|
-
);
|
|
2734
|
-
return DEFAULT_SOUL;
|
|
2759
|
+
function findSplitIndex(text, maxChars) {
|
|
2760
|
+
if (text.length <= maxChars) {
|
|
2761
|
+
return text.length;
|
|
2735
2762
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
return
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2763
|
+
const bounded = text.slice(0, maxChars);
|
|
2764
|
+
const newlineIndex = bounded.lastIndexOf("\n");
|
|
2765
|
+
if (newlineIndex > 0) {
|
|
2766
|
+
return newlineIndex;
|
|
2767
|
+
}
|
|
2768
|
+
const spaceIndex = bounded.lastIndexOf(" ");
|
|
2769
|
+
if (spaceIndex > 0) {
|
|
2770
|
+
return spaceIndex;
|
|
2771
|
+
}
|
|
2772
|
+
return maxChars;
|
|
2773
|
+
}
|
|
2774
|
+
function splitByLineBudget(text, maxLines) {
|
|
2775
|
+
if (maxLines <= 0) {
|
|
2776
|
+
return "";
|
|
2777
|
+
}
|
|
2778
|
+
const lines = text.split("\n");
|
|
2779
|
+
if (lines.length <= maxLines) {
|
|
2780
|
+
return text;
|
|
2781
|
+
}
|
|
2782
|
+
return lines.slice(0, maxLines).join("\n");
|
|
2783
|
+
}
|
|
2784
|
+
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
2785
|
+
return {
|
|
2786
|
+
maxChars: Math.max(1, maxChars - suffix.length),
|
|
2787
|
+
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function forceSplitBudget(text, budget) {
|
|
2791
|
+
const lineCount = countSlackLines(text);
|
|
2792
|
+
return {
|
|
2793
|
+
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
2794
|
+
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
function getFenceContinuation(text) {
|
|
2798
|
+
let open;
|
|
2799
|
+
for (const line of text.split("\n")) {
|
|
2800
|
+
const trimmed = line.trimStart();
|
|
2801
|
+
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
2802
|
+
if (!openerMatch) {
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (!open) {
|
|
2806
|
+
open = {
|
|
2807
|
+
fence: openerMatch[1],
|
|
2808
|
+
openerLine: trimmed
|
|
2809
|
+
};
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
|
|
2813
|
+
open = void 0;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (!open) {
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
closeSuffix: text.endsWith("\n") ? open.fence : `
|
|
2821
|
+
${open.fence}`,
|
|
2822
|
+
reopenPrefix: `${open.openerLine}
|
|
2823
|
+
`
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
function appendSlackSuffix(text, marker) {
|
|
2827
|
+
const carryover = getFenceContinuation(text);
|
|
2828
|
+
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
2829
|
+
}
|
|
2830
|
+
function takeSlackContinuationChunk(text, budget) {
|
|
2831
|
+
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
2832
|
+
if (!rest) {
|
|
2833
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
2834
|
+
text,
|
|
2835
|
+
forceSplitBudget(text, budget)
|
|
2836
|
+
));
|
|
2837
|
+
}
|
|
2838
|
+
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
2839
|
+
if (!carryover) {
|
|
2840
|
+
return { prefix, rest };
|
|
2841
|
+
}
|
|
2842
|
+
const carryoverBudget = reserveInlineBudgetForSuffix(
|
|
2843
|
+
`${carryover.closeSuffix}${CONTINUED_MARKER}`
|
|
2844
|
+
);
|
|
2845
|
+
({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
|
|
2846
|
+
if (!rest) {
|
|
2847
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
2848
|
+
text,
|
|
2849
|
+
forceSplitBudget(text, carryoverBudget)
|
|
2850
|
+
));
|
|
2851
|
+
}
|
|
2852
|
+
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
2853
|
+
if (!carryover) {
|
|
2854
|
+
return { prefix, rest };
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
prefix,
|
|
2858
|
+
rest: `${carryover.reopenPrefix}${rest}`
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
function takeSlackContinuationPrefix(text, options) {
|
|
2862
|
+
const budget = {
|
|
2863
|
+
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
2864
|
+
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
2865
|
+
};
|
|
2866
|
+
const { prefix, rest } = (() => {
|
|
2867
|
+
if (options?.forceSplit) {
|
|
2868
|
+
return takeSlackContinuationChunk(text, budget);
|
|
2869
|
+
}
|
|
2870
|
+
const initial = takeSlackInlinePrefix(text, budget);
|
|
2871
|
+
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
2872
|
+
})();
|
|
2873
|
+
return {
|
|
2874
|
+
prefix,
|
|
2875
|
+
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
2876
|
+
rest
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
function takeSlackInlinePrefix(text, options) {
|
|
2880
|
+
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
2881
|
+
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
2882
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
2883
|
+
if (!normalized) {
|
|
2884
|
+
return { prefix: "", rest: "" };
|
|
2885
|
+
}
|
|
2886
|
+
if (fitsInlineBudget(normalized, maxChars, maxLines)) {
|
|
2887
|
+
return { prefix: normalized, rest: "" };
|
|
2888
|
+
}
|
|
2889
|
+
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
2890
|
+
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
2891
|
+
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
2892
|
+
if (prefix) {
|
|
2893
|
+
return {
|
|
2894
|
+
prefix,
|
|
2895
|
+
rest: normalized.slice(prefix.length).trimStart()
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
2899
|
+
return {
|
|
2900
|
+
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
2901
|
+
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function splitSlackReplyText(text, options) {
|
|
2905
|
+
const normalized = normalizeForSlack(text);
|
|
2906
|
+
if (!normalized) {
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const chunks = [];
|
|
2910
|
+
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2911
|
+
let remaining = normalized;
|
|
2912
|
+
while (remaining) {
|
|
2913
|
+
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, INTERRUPTED_MARKER)) : fitsInlineBudget(remaining);
|
|
2914
|
+
if (fitsFinalChunk) {
|
|
2915
|
+
chunks.push(
|
|
2916
|
+
options?.interrupted ? appendSlackSuffix(remaining, INTERRUPTED_MARKER) : remaining
|
|
2917
|
+
);
|
|
2918
|
+
break;
|
|
2919
|
+
}
|
|
2920
|
+
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
2921
|
+
...continuationBudget,
|
|
2922
|
+
forceSplit: true
|
|
2923
|
+
});
|
|
2924
|
+
chunks.push(renderedPrefix);
|
|
2925
|
+
remaining = rest;
|
|
2926
|
+
}
|
|
2927
|
+
return chunks;
|
|
2928
|
+
}
|
|
2929
|
+
function getSlackInterruptionMarker() {
|
|
2930
|
+
return INTERRUPTED_MARKER;
|
|
2931
|
+
}
|
|
2932
|
+
function getSlackContinuationBudget() {
|
|
2933
|
+
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2934
|
+
}
|
|
2935
|
+
function getSlackStreamingContinuationBudget() {
|
|
2936
|
+
return reserveInlineBudgetForSuffix(
|
|
2937
|
+
`${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
function buildSlackOutputMessage(text, files) {
|
|
2941
|
+
const normalized = normalizeForSlack(text);
|
|
2942
|
+
const fileCount = files?.length ?? 0;
|
|
2943
|
+
if (!normalized) {
|
|
2944
|
+
if (fileCount > 0) {
|
|
2945
|
+
return {
|
|
2946
|
+
raw: "",
|
|
2947
|
+
files
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
logWarn(
|
|
2951
|
+
"slack_output_normalized_empty",
|
|
2952
|
+
{},
|
|
2953
|
+
{
|
|
2954
|
+
"app.output.original_length": text.length,
|
|
2955
|
+
"app.output.parsed_length": normalized.length,
|
|
2956
|
+
"app.output.file_count": fileCount
|
|
2957
|
+
},
|
|
2958
|
+
"Slack output normalized to empty content"
|
|
2959
|
+
);
|
|
2960
|
+
return {
|
|
2961
|
+
markdown: "I couldn't produce a response.",
|
|
2962
|
+
files
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
return {
|
|
2966
|
+
markdown: normalized,
|
|
2967
|
+
files
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
var slackOutputPolicy = {
|
|
2971
|
+
maxInlineChars: MAX_INLINE_CHARS,
|
|
2972
|
+
maxInlineLines: MAX_INLINE_LINES
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
// src/chat/prompt.ts
|
|
2976
|
+
var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
|
|
2977
|
+
function getLoggedMarkdownFiles() {
|
|
2978
|
+
const globalState = globalThis;
|
|
2979
|
+
globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
|
|
2980
|
+
return globalState.__juniorLoggedMarkdownFiles;
|
|
2981
|
+
}
|
|
2982
|
+
function loadOptionalMarkdownFile(candidates, fileName) {
|
|
2983
|
+
for (const resolved of candidates) {
|
|
2984
|
+
try {
|
|
2985
|
+
const raw = fs.readFileSync(resolved, "utf8").trim();
|
|
2986
|
+
if (raw.length > 0) {
|
|
2987
|
+
const loggedMarkdownFiles = getLoggedMarkdownFiles();
|
|
2988
|
+
const logKey = `${fileName}:${resolved}`;
|
|
2989
|
+
if (!loggedMarkdownFiles.has(logKey)) {
|
|
2990
|
+
loggedMarkdownFiles.add(logKey);
|
|
2991
|
+
logInfo(
|
|
2992
|
+
`${fileName.toLowerCase()}_loaded`,
|
|
2993
|
+
{},
|
|
2994
|
+
{
|
|
2995
|
+
"file.path": resolved
|
|
2996
|
+
},
|
|
2997
|
+
`Loaded ${fileName}`
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
return raw;
|
|
3001
|
+
}
|
|
3002
|
+
} catch {
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
function loadSoul() {
|
|
3009
|
+
const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
|
|
3010
|
+
if (soul) {
|
|
3011
|
+
return soul;
|
|
3012
|
+
}
|
|
3013
|
+
logWarn(
|
|
3014
|
+
"soul_load_fallback",
|
|
3015
|
+
{},
|
|
3016
|
+
{
|
|
3017
|
+
"file.candidates": soulPathCandidates()
|
|
3018
|
+
},
|
|
3019
|
+
"SOUL.md not found; using built-in default personality"
|
|
3020
|
+
);
|
|
3021
|
+
return DEFAULT_SOUL;
|
|
3022
|
+
}
|
|
3023
|
+
function loadWorld() {
|
|
3024
|
+
return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
|
|
3025
|
+
}
|
|
3026
|
+
var JUNIOR_PERSONALITY = (() => {
|
|
3027
|
+
try {
|
|
3028
|
+
return loadSoul();
|
|
3029
|
+
} catch (error) {
|
|
3030
|
+
logWarn(
|
|
3031
|
+
"soul_load_failed",
|
|
3032
|
+
{},
|
|
3033
|
+
{
|
|
3034
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
3035
|
+
},
|
|
3036
|
+
"Failed to load SOUL.md; using built-in default personality"
|
|
3037
|
+
);
|
|
3038
|
+
return DEFAULT_SOUL;
|
|
3039
|
+
}
|
|
3040
|
+
})();
|
|
3041
|
+
var JUNIOR_WORLD = (() => {
|
|
3042
|
+
try {
|
|
3043
|
+
return loadWorld();
|
|
3044
|
+
} catch (error) {
|
|
3045
|
+
logWarn(
|
|
3046
|
+
"world_load_failed",
|
|
3047
|
+
{},
|
|
3048
|
+
{
|
|
3049
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
2746
3050
|
},
|
|
2747
3051
|
"Failed to load WORLD.md; omitting world prompt context"
|
|
2748
3052
|
);
|
|
@@ -3039,6 +3343,9 @@ function buildSystemPrompt(params) {
|
|
|
3039
3343
|
"- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
|
|
3040
3344
|
"- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
|
|
3041
3345
|
"- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
|
|
3346
|
+
"- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
|
|
3347
|
+
"- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
|
|
3348
|
+
"- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
|
|
3042
3349
|
"- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
|
|
3043
3350
|
"- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
|
|
3044
3351
|
"- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
|
|
@@ -3055,14 +3362,14 @@ function buildSystemPrompt(params) {
|
|
|
3055
3362
|
"- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
|
|
3056
3363
|
"- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `eyes`, not unicode emoji), and avoid redundant acknowledgment text.",
|
|
3057
3364
|
"- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
|
|
3058
|
-
"- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--
|
|
3365
|
+
"- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--target <value>]` as a bash command before authenticated bash/API work for that skill.",
|
|
3059
3366
|
"- Use the minimum declared capability needed for the current operation.",
|
|
3060
3367
|
"- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
|
|
3061
3368
|
"- For disconnect + reconnect requests, run `jr-rpc delete-token <provider>` first, then `jr-rpc issue-credential` \u2014 the system handles the reconnect without auto-resuming the reconnect message.",
|
|
3062
3369
|
"- Use `jr-rpc oauth-start <provider>` only when the user explicitly asks to connect a provider and there is no task to resume after authorization.",
|
|
3063
|
-
"-
|
|
3064
|
-
"- To persist or read conversation defaults
|
|
3065
|
-
"- Capabilities
|
|
3370
|
+
"- Provider-targeted capabilities may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
|
|
3371
|
+
"- To persist or read conversation defaults, choose a config key from the provider catalog or active skill metadata and run `jr-rpc config get|set|unset|list ...` as a bash command.",
|
|
3372
|
+
"- Capabilities must match the exact provider-qualified tokens declared by the loaded skill or provider catalog.",
|
|
3066
3373
|
"- When your work is complete, provide the exact user-facing markdown response.",
|
|
3067
3374
|
"- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
|
|
3068
3375
|
"- Prefer `webSearch` before `webFetch` when the user gave no URL.",
|
|
@@ -3097,7 +3404,8 @@ function buildSystemPrompt(params) {
|
|
|
3097
3404
|
"- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
|
|
3098
3405
|
"- Keep normal responses brief and scannable.",
|
|
3099
3406
|
"- If depth is needed, start with a concise summary and then provide fuller detail.",
|
|
3100
|
-
"-
|
|
3407
|
+
"- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
|
|
3408
|
+
"- Do not narrate tool execution or repeated status updates in the visible reply.",
|
|
3101
3409
|
"- Avoid tables unless explicitly requested.",
|
|
3102
3410
|
"- End every turn with a final user-facing markdown response.",
|
|
3103
3411
|
"</output>"
|
|
@@ -3134,40 +3442,49 @@ var ProviderCredentialRouter = class {
|
|
|
3134
3442
|
};
|
|
3135
3443
|
|
|
3136
3444
|
// src/chat/capabilities/target.ts
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3445
|
+
function escapeRegExp(value) {
|
|
3446
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3447
|
+
}
|
|
3448
|
+
function normalizeTargetValue(value) {
|
|
3449
|
+
let normalized = value.trim();
|
|
3450
|
+
if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
|
|
3451
|
+
normalized = normalized.slice(1, -1).trim();
|
|
3452
|
+
}
|
|
3453
|
+
return normalized || void 0;
|
|
3454
|
+
}
|
|
3455
|
+
function extractFlagValue(text, flags) {
|
|
3456
|
+
if (flags.length === 0) {
|
|
3141
3457
|
return void 0;
|
|
3142
3458
|
}
|
|
3143
|
-
const
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3459
|
+
const pattern = flags.map(escapeRegExp).join("|");
|
|
3460
|
+
const match = new RegExp(
|
|
3461
|
+
String.raw`(?:^|\s)(?:${pattern})(?:\s+|=)([^\s]+)`
|
|
3462
|
+
).exec(text);
|
|
3463
|
+
return match ? normalizeTargetValue(match[1] ?? "") : void 0;
|
|
3464
|
+
}
|
|
3465
|
+
function createCapabilityTarget(type, value) {
|
|
3466
|
+
const normalizedType = type.trim();
|
|
3467
|
+
const normalizedValue = normalizeTargetValue(value);
|
|
3468
|
+
if (!normalizedType || !normalizedValue) {
|
|
3146
3469
|
return void 0;
|
|
3147
3470
|
}
|
|
3148
3471
|
return {
|
|
3149
|
-
|
|
3150
|
-
|
|
3472
|
+
type: normalizedType,
|
|
3473
|
+
value: normalizedValue
|
|
3151
3474
|
};
|
|
3152
3475
|
}
|
|
3153
|
-
function extractRepoRef(text) {
|
|
3154
|
-
const byFlag = REPO_FLAG_RE.exec(text);
|
|
3155
|
-
if (byFlag) {
|
|
3156
|
-
return parseRepoTarget(byFlag[1]);
|
|
3157
|
-
}
|
|
3158
|
-
return void 0;
|
|
3159
|
-
}
|
|
3160
3476
|
function extractCapabilityTarget(params) {
|
|
3477
|
+
const flags = params.target.commandFlags ?? [];
|
|
3161
3478
|
if (params.commandText) {
|
|
3162
|
-
const
|
|
3163
|
-
if (
|
|
3164
|
-
return
|
|
3479
|
+
const value = extractFlagValue(params.commandText, flags);
|
|
3480
|
+
if (value) {
|
|
3481
|
+
return createCapabilityTarget(params.target.type, value);
|
|
3165
3482
|
}
|
|
3166
3483
|
}
|
|
3167
3484
|
if (params.invocationArgs) {
|
|
3168
|
-
const
|
|
3169
|
-
if (
|
|
3170
|
-
return
|
|
3485
|
+
const value = extractFlagValue(params.invocationArgs, flags);
|
|
3486
|
+
if (value) {
|
|
3487
|
+
return createCapabilityTarget(params.target.type, value);
|
|
3171
3488
|
}
|
|
3172
3489
|
}
|
|
3173
3490
|
return void 0;
|
|
@@ -3198,44 +3515,50 @@ var SkillCapabilityRuntime = class {
|
|
|
3198
3515
|
}
|
|
3199
3516
|
async resolveCapabilityTarget(input) {
|
|
3200
3517
|
const activeSkill = input.activeSkill;
|
|
3201
|
-
const explicitTarget = input.
|
|
3518
|
+
const explicitTarget = input.targetRef ? createCapabilityTarget(input.target.type, input.targetRef) : void 0;
|
|
3202
3519
|
if (explicitTarget) {
|
|
3203
3520
|
return explicitTarget;
|
|
3204
3521
|
}
|
|
3205
3522
|
const inferredTarget = extractCapabilityTarget({
|
|
3206
|
-
invocationArgs: this.invocationArgs
|
|
3523
|
+
invocationArgs: this.invocationArgs,
|
|
3524
|
+
target: input.target
|
|
3207
3525
|
});
|
|
3208
3526
|
if (inferredTarget) {
|
|
3209
3527
|
return inferredTarget;
|
|
3210
3528
|
}
|
|
3211
|
-
if (!
|
|
3529
|
+
if (!this.resolveConfiguration) {
|
|
3212
3530
|
return void 0;
|
|
3213
3531
|
}
|
|
3214
|
-
const
|
|
3215
|
-
|
|
3532
|
+
const configuredValue = await this.resolveConfiguration(
|
|
3533
|
+
input.target.configKey
|
|
3534
|
+
);
|
|
3535
|
+
if (typeof configuredValue !== "string" || configuredValue.trim().length === 0) {
|
|
3216
3536
|
return void 0;
|
|
3217
3537
|
}
|
|
3218
|
-
const configuredTarget =
|
|
3538
|
+
const configuredTarget = createCapabilityTarget(
|
|
3539
|
+
input.target.type,
|
|
3540
|
+
configuredValue
|
|
3541
|
+
);
|
|
3219
3542
|
if (!configuredTarget) {
|
|
3220
3543
|
logWarn(
|
|
3221
3544
|
"config_value_invalid_for_capability_target",
|
|
3222
3545
|
{},
|
|
3223
3546
|
{
|
|
3224
3547
|
"app.skill.name": activeSkill?.name,
|
|
3225
|
-
"app.config.key": input.configKey
|
|
3548
|
+
"app.config.key": input.target.configKey
|
|
3226
3549
|
},
|
|
3227
|
-
`Configured ${input.configKey} is invalid for capability target resolution`
|
|
3550
|
+
`Configured ${input.target.configKey} is invalid for capability target resolution`
|
|
3228
3551
|
);
|
|
3229
3552
|
return void 0;
|
|
3230
3553
|
}
|
|
3231
3554
|
const declaredConfig = activeSkill?.usesConfig ?? [];
|
|
3232
|
-
if (activeSkill && !declaredConfig.includes(input.configKey)) {
|
|
3555
|
+
if (activeSkill && !declaredConfig.includes(input.target.configKey)) {
|
|
3233
3556
|
logWarn(
|
|
3234
3557
|
"config_key_not_declared_for_skill",
|
|
3235
3558
|
{},
|
|
3236
3559
|
{
|
|
3237
3560
|
"app.skill.name": activeSkill.name,
|
|
3238
|
-
"app.config.key": input.configKey
|
|
3561
|
+
"app.config.key": input.target.configKey
|
|
3239
3562
|
},
|
|
3240
3563
|
"Configuration key used by runtime is not declared in active skill frontmatter (soft enforcement)"
|
|
3241
3564
|
);
|
|
@@ -3243,9 +3566,7 @@ var SkillCapabilityRuntime = class {
|
|
|
3243
3566
|
return configuredTarget;
|
|
3244
3567
|
}
|
|
3245
3568
|
capabilityCacheKey(capability, target) {
|
|
3246
|
-
const
|
|
3247
|
-
const repo = target?.repo?.trim().toLowerCase();
|
|
3248
|
-
const scope = owner && repo ? `${owner}/${repo}` : "none";
|
|
3569
|
+
const scope = target ? `${target.type}:${target.value.trim()}` : "none";
|
|
3249
3570
|
return `${capability}:${scope}`;
|
|
3250
3571
|
}
|
|
3251
3572
|
async issueCapabilityLease(input) {
|
|
@@ -3256,10 +3577,10 @@ var SkillCapabilityRuntime = class {
|
|
|
3256
3577
|
);
|
|
3257
3578
|
}
|
|
3258
3579
|
const activeSkill = input.activeSkill;
|
|
3259
|
-
const target = capabilityProvider.target
|
|
3580
|
+
const target = capabilityProvider.target ? await this.resolveCapabilityTarget({
|
|
3260
3581
|
activeSkill,
|
|
3261
|
-
|
|
3262
|
-
|
|
3582
|
+
target: capabilityProvider.target,
|
|
3583
|
+
targetRef: input.targetRef
|
|
3263
3584
|
}) : void 0;
|
|
3264
3585
|
return await this.router.issue({
|
|
3265
3586
|
capability: input.capability,
|
|
@@ -3294,14 +3615,14 @@ var SkillCapabilityRuntime = class {
|
|
|
3294
3615
|
);
|
|
3295
3616
|
}
|
|
3296
3617
|
const activeSkill = input.activeSkill;
|
|
3297
|
-
const capabilityTarget = capabilityProvider.target
|
|
3618
|
+
const capabilityTarget = capabilityProvider.target ? await this.resolveCapabilityTarget({
|
|
3298
3619
|
activeSkill,
|
|
3299
|
-
|
|
3300
|
-
|
|
3620
|
+
target: capabilityProvider.target,
|
|
3621
|
+
targetRef: input.targetRef
|
|
3301
3622
|
}) : void 0;
|
|
3302
|
-
if (capabilityProvider.target
|
|
3623
|
+
if (capabilityProvider.target && !capabilityTarget?.value.trim()) {
|
|
3303
3624
|
throw new Error(
|
|
3304
|
-
|
|
3625
|
+
`jr-rpc issue-credential requires ${capabilityProvider.target.type} target context; use --target <value>`
|
|
3305
3626
|
);
|
|
3306
3627
|
}
|
|
3307
3628
|
const declared = activeSkill?.requiresCapabilities ?? [];
|
|
@@ -3338,7 +3659,7 @@ var SkillCapabilityRuntime = class {
|
|
|
3338
3659
|
const lease = await this.issueCapabilityLease({
|
|
3339
3660
|
activeSkill,
|
|
3340
3661
|
capability,
|
|
3341
|
-
|
|
3662
|
+
targetRef: input.targetRef,
|
|
3342
3663
|
reason: input.reason
|
|
3343
3664
|
});
|
|
3344
3665
|
const transforms = this.toHeaderTransforms(lease);
|
|
@@ -3465,7 +3786,7 @@ var TestCredentialBroker = class {
|
|
|
3465
3786
|
expiresAt,
|
|
3466
3787
|
metadata: {
|
|
3467
3788
|
reason: input.reason,
|
|
3468
|
-
target: input.target
|
|
3789
|
+
target: input.target ? `${input.target.type}:${input.target.value}` : "none"
|
|
3469
3790
|
}
|
|
3470
3791
|
};
|
|
3471
3792
|
}
|
|
@@ -3568,24 +3889,24 @@ async function handleIssueCredentialCommand(args, deps) {
|
|
|
3568
3889
|
exitCode: 2
|
|
3569
3890
|
});
|
|
3570
3891
|
}
|
|
3571
|
-
let
|
|
3892
|
+
let targetRef;
|
|
3572
3893
|
const extras = args.slice(1);
|
|
3573
3894
|
if (extras.length > 0) {
|
|
3574
|
-
if (extras.length === 2 && extras[0] === "--
|
|
3575
|
-
|
|
3576
|
-
} else if (extras.length === 1 && extras[0].startsWith("--
|
|
3577
|
-
|
|
3895
|
+
if (extras.length === 2 && extras[0] === "--target") {
|
|
3896
|
+
targetRef = extras[1]?.trim();
|
|
3897
|
+
} else if (extras.length === 1 && extras[0].startsWith("--target=")) {
|
|
3898
|
+
targetRef = extras[0].slice("--target=".length).trim();
|
|
3578
3899
|
} else {
|
|
3579
3900
|
return {
|
|
3580
3901
|
stdout: "",
|
|
3581
|
-
stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --
|
|
3902
|
+
stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --target <value>\n",
|
|
3582
3903
|
exitCode: 2
|
|
3583
3904
|
};
|
|
3584
3905
|
}
|
|
3585
|
-
if (!
|
|
3906
|
+
if (!targetRef) {
|
|
3586
3907
|
return {
|
|
3587
3908
|
stdout: "",
|
|
3588
|
-
stderr: "jr-rpc issue-credential --
|
|
3909
|
+
stderr: "jr-rpc issue-credential --target requires a non-empty value\n",
|
|
3589
3910
|
exitCode: 2
|
|
3590
3911
|
};
|
|
3591
3912
|
}
|
|
@@ -3595,7 +3916,7 @@ async function handleIssueCredentialCommand(args, deps) {
|
|
|
3595
3916
|
outcome = await deps.capabilityRuntime.enableCapabilityForTurn({
|
|
3596
3917
|
activeSkill: deps.activeSkill,
|
|
3597
3918
|
capability,
|
|
3598
|
-
...
|
|
3919
|
+
...targetRef ? { targetRef } : {},
|
|
3599
3920
|
reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
|
|
3600
3921
|
});
|
|
3601
3922
|
} catch (error) {
|
|
@@ -3985,7 +4306,7 @@ async function handleDeleteTokenCommand(args, deps) {
|
|
|
3985
4306
|
function createJrRpcCommand(deps) {
|
|
3986
4307
|
return defineCommand("jr-rpc", async (args) => {
|
|
3987
4308
|
const usage = [
|
|
3988
|
-
"jr-rpc issue-credential <capability> [--
|
|
4309
|
+
"jr-rpc issue-credential <capability> [--target <value>]",
|
|
3989
4310
|
"jr-rpc oauth-start <provider>",
|
|
3990
4311
|
"jr-rpc delete-token <provider>",
|
|
3991
4312
|
"jr-rpc config get <key>",
|
|
@@ -5778,7 +6099,7 @@ function createSlackCanvasCreateTool(context, state) {
|
|
|
5778
6099
|
channelId: targetChannelId
|
|
5779
6100
|
});
|
|
5780
6101
|
state.setTurnCreatedCanvasId(created.canvasId);
|
|
5781
|
-
state.patchArtifactState({
|
|
6102
|
+
await state.patchArtifactState({
|
|
5782
6103
|
lastCanvasId: created.canvasId,
|
|
5783
6104
|
lastCanvasUrl: created.permalink,
|
|
5784
6105
|
recentCanvases: mergeRecentCanvases(
|
|
@@ -5877,7 +6198,7 @@ function createSlackCanvasUpdateTool(state, _context) {
|
|
|
5877
6198
|
operation: resolvedOperation,
|
|
5878
6199
|
sectionId
|
|
5879
6200
|
});
|
|
5880
|
-
state.patchArtifactState({ lastCanvasId: targetCanvasId });
|
|
6201
|
+
await state.patchArtifactState({ lastCanvasId: targetCanvasId });
|
|
5881
6202
|
const response = {
|
|
5882
6203
|
ok: true,
|
|
5883
6204
|
canvas_id: targetCanvasId,
|
|
@@ -6082,7 +6403,7 @@ function createSlackListCreateTool(state) {
|
|
|
6082
6403
|
};
|
|
6083
6404
|
}
|
|
6084
6405
|
const list = await createTodoList(name);
|
|
6085
|
-
state.patchArtifactState({
|
|
6406
|
+
await state.patchArtifactState({
|
|
6086
6407
|
lastListId: list.listId,
|
|
6087
6408
|
lastListUrl: list.permalink,
|
|
6088
6409
|
listColumnMap: list.listColumnMap
|
|
@@ -6145,7 +6466,7 @@ function createSlackListAddItemsTool(state) {
|
|
|
6145
6466
|
assigneeUserId: assignee_user_id,
|
|
6146
6467
|
dueDate: due_date
|
|
6147
6468
|
});
|
|
6148
|
-
state.patchArtifactState({
|
|
6469
|
+
await state.patchArtifactState({
|
|
6149
6470
|
lastListId: targetListId,
|
|
6150
6471
|
listColumnMap: result.listColumnMap
|
|
6151
6472
|
});
|
|
@@ -6237,7 +6558,7 @@ function createSlackListUpdateItemTool(state) {
|
|
|
6237
6558
|
title,
|
|
6238
6559
|
listColumnMap: state.artifactState.listColumnMap ?? {}
|
|
6239
6560
|
});
|
|
6240
|
-
state.patchArtifactState({ lastListId: targetListId });
|
|
6561
|
+
await state.patchArtifactState({ lastListId: targetListId });
|
|
6241
6562
|
const response = {
|
|
6242
6563
|
ok: true,
|
|
6243
6564
|
list_id: targetListId,
|
|
@@ -6838,7 +7159,7 @@ function createToolState(hooks, context) {
|
|
|
6838
7159
|
...context.artifactState?.listColumnMap ?? {}
|
|
6839
7160
|
}
|
|
6840
7161
|
};
|
|
6841
|
-
const patchArtifactState = (patch) => {
|
|
7162
|
+
const patchArtifactState = async (patch) => {
|
|
6842
7163
|
Object.assign(artifactState, patch);
|
|
6843
7164
|
if (patch.listColumnMap) {
|
|
6844
7165
|
artifactState.listColumnMap = {
|
|
@@ -6846,7 +7167,7 @@ function createToolState(hooks, context) {
|
|
|
6846
7167
|
...patch.listColumnMap
|
|
6847
7168
|
};
|
|
6848
7169
|
}
|
|
6849
|
-
hooks.onArtifactStatePatch?.(patch);
|
|
7170
|
+
await hooks.onArtifactStatePatch?.(patch);
|
|
6850
7171
|
};
|
|
6851
7172
|
return {
|
|
6852
7173
|
artifactState,
|
|
@@ -7955,10 +8276,14 @@ function createSandboxSessionManager(options) {
|
|
|
7955
8276
|
sandboxIdHint = void 0;
|
|
7956
8277
|
toolExecutors = void 0;
|
|
7957
8278
|
};
|
|
7958
|
-
const rememberSandbox = (nextSandbox) => {
|
|
8279
|
+
const rememberSandbox = async (nextSandbox) => {
|
|
7959
8280
|
sandbox = nextSandbox;
|
|
7960
8281
|
sandboxIdHint = nextSandbox.sandboxId;
|
|
7961
8282
|
toolExecutors = void 0;
|
|
8283
|
+
await options?.onSandboxAcquired?.({
|
|
8284
|
+
sandboxId: nextSandbox.sandboxId,
|
|
8285
|
+
...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
|
|
8286
|
+
});
|
|
7962
8287
|
return nextSandbox;
|
|
7963
8288
|
};
|
|
7964
8289
|
const failSetup = (error) => {
|
|
@@ -8148,7 +8473,7 @@ function createSandboxSessionManager(options) {
|
|
|
8148
8473
|
} catch (error) {
|
|
8149
8474
|
return failSetup(error);
|
|
8150
8475
|
}
|
|
8151
|
-
return rememberSandbox(createdSandbox);
|
|
8476
|
+
return await rememberSandbox(createdSandbox);
|
|
8152
8477
|
};
|
|
8153
8478
|
const discardHintIfProfileChanged = () => {
|
|
8154
8479
|
if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
|
|
@@ -8203,7 +8528,7 @@ function createSandboxSessionManager(options) {
|
|
|
8203
8528
|
}
|
|
8204
8529
|
try {
|
|
8205
8530
|
await syncSkills(hintedSandbox);
|
|
8206
|
-
return rememberSandbox(hintedSandbox);
|
|
8531
|
+
return await rememberSandbox(hintedSandbox);
|
|
8207
8532
|
} catch (error) {
|
|
8208
8533
|
if (isSandboxUnavailableError(error)) {
|
|
8209
8534
|
return await recreateUnavailableSandbox("id_hint");
|
|
@@ -8450,7 +8775,8 @@ function createSandboxExecutor(options) {
|
|
|
8450
8775
|
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
8451
8776
|
timeoutMs: options?.timeoutMs,
|
|
8452
8777
|
traceContext,
|
|
8453
|
-
onStatus: options?.onStatus
|
|
8778
|
+
onStatus: options?.onStatus,
|
|
8779
|
+
onSandboxAcquired: options?.onSandboxAcquired
|
|
8454
8780
|
});
|
|
8455
8781
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
8456
8782
|
const logSandboxBootRequest = (trigger, details = {}) => {
|
|
@@ -8699,13 +9025,13 @@ function buildToolStatus(toolName, input) {
|
|
|
8699
9025
|
return makeAssistantStatus("loading", skillName);
|
|
8700
9026
|
}
|
|
8701
9027
|
if (query && toolName === "webSearch") {
|
|
8702
|
-
return makeAssistantStatus("searching",
|
|
8703
|
-
}
|
|
8704
|
-
if (query && provider && toolName === "searchTools") {
|
|
8705
|
-
return makeAssistantStatus("searching", `${provider} "${query}"`);
|
|
9028
|
+
return makeAssistantStatus("searching", "sources");
|
|
8706
9029
|
}
|
|
8707
9030
|
if (query && toolName === "searchTools") {
|
|
8708
|
-
return makeAssistantStatus(
|
|
9031
|
+
return makeAssistantStatus(
|
|
9032
|
+
"searching",
|
|
9033
|
+
provider ? `${provider} tools` : "tools"
|
|
9034
|
+
);
|
|
8709
9035
|
}
|
|
8710
9036
|
if (domain && toolName === "webFetch") {
|
|
8711
9037
|
return makeAssistantStatus("fetching", domain);
|
|
@@ -8964,66 +9290,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
8964
9290
|
}));
|
|
8965
9291
|
}
|
|
8966
9292
|
|
|
8967
|
-
// src/chat/runtime/turn.ts
|
|
8968
|
-
function buildDeterministicTurnId(messageId) {
|
|
8969
|
-
const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
8970
|
-
return `turn_${sanitized}`;
|
|
8971
|
-
}
|
|
8972
|
-
var RetryableTurnError = class extends Error {
|
|
8973
|
-
code = "retryable_turn";
|
|
8974
|
-
reason;
|
|
8975
|
-
constructor(reason, message) {
|
|
8976
|
-
super(message);
|
|
8977
|
-
this.name = "RetryableTurnError";
|
|
8978
|
-
this.reason = reason;
|
|
8979
|
-
}
|
|
8980
|
-
};
|
|
8981
|
-
function isRetryableTurnError(error, reason) {
|
|
8982
|
-
if (!(error instanceof RetryableTurnError)) {
|
|
8983
|
-
return false;
|
|
8984
|
-
}
|
|
8985
|
-
if (!reason) {
|
|
8986
|
-
return true;
|
|
8987
|
-
}
|
|
8988
|
-
return error.reason === reason;
|
|
8989
|
-
}
|
|
8990
|
-
function startActiveTurn(args) {
|
|
8991
|
-
args.conversation.processing.activeTurnId = args.nextTurnId;
|
|
8992
|
-
args.updateConversationStats(args.conversation);
|
|
8993
|
-
}
|
|
8994
|
-
function markTurnCompleted(args) {
|
|
8995
|
-
args.conversation.processing.activeTurnId = void 0;
|
|
8996
|
-
args.conversation.processing.lastCompletedAtMs = args.nowMs;
|
|
8997
|
-
args.updateConversationStats(args.conversation);
|
|
8998
|
-
}
|
|
8999
|
-
function markTurnFailed(args) {
|
|
9000
|
-
args.conversation.processing.activeTurnId = void 0;
|
|
9001
|
-
args.conversation.processing.lastCompletedAtMs = args.nowMs;
|
|
9002
|
-
args.markConversationMessage(args.conversation, args.userMessageId, {
|
|
9003
|
-
replied: false,
|
|
9004
|
-
skippedReason: "reply failed"
|
|
9005
|
-
});
|
|
9006
|
-
args.updateConversationStats(args.conversation);
|
|
9007
|
-
}
|
|
9008
|
-
function resolveReplyDelivery(args) {
|
|
9009
|
-
const replyHasFiles = Boolean(
|
|
9010
|
-
args.reply.files && args.reply.files.length > 0
|
|
9011
|
-
);
|
|
9012
|
-
const deliveryPlan = args.reply.deliveryPlan ?? {
|
|
9013
|
-
mode: args.reply.deliveryMode ?? "thread",
|
|
9014
|
-
postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
|
|
9015
|
-
attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
|
|
9016
|
-
};
|
|
9017
|
-
let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
|
|
9018
|
-
if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
|
|
9019
|
-
attachFiles = "inline";
|
|
9020
|
-
}
|
|
9021
|
-
return {
|
|
9022
|
-
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
9023
|
-
attachFiles
|
|
9024
|
-
};
|
|
9025
|
-
}
|
|
9026
|
-
|
|
9027
9293
|
// src/chat/services/reply-delivery-plan.ts
|
|
9028
9294
|
var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
|
|
9029
9295
|
var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
|
|
@@ -9069,8 +9335,29 @@ function buildReplyDeliveryPlan(args) {
|
|
|
9069
9335
|
attachFiles
|
|
9070
9336
|
};
|
|
9071
9337
|
}
|
|
9072
|
-
|
|
9073
|
-
|
|
9338
|
+
function resolveReplyDelivery(args) {
|
|
9339
|
+
const replyHasFiles = Boolean(
|
|
9340
|
+
args.reply.files && args.reply.files.length > 0
|
|
9341
|
+
);
|
|
9342
|
+
const deliveryPlan = args.reply.deliveryPlan ?? {
|
|
9343
|
+
mode: args.reply.deliveryMode ?? "thread",
|
|
9344
|
+
postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
|
|
9345
|
+
attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
|
|
9346
|
+
};
|
|
9347
|
+
let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
|
|
9348
|
+
if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
|
|
9349
|
+
attachFiles = "inline";
|
|
9350
|
+
}
|
|
9351
|
+
if (attachFiles === "inline" && args.hasStreamedThreadReply) {
|
|
9352
|
+
attachFiles = "followup";
|
|
9353
|
+
}
|
|
9354
|
+
return {
|
|
9355
|
+
shouldPostThreadReply: deliveryPlan.postThreadText,
|
|
9356
|
+
attachFiles
|
|
9357
|
+
};
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
// src/chat/services/channel-intent.ts
|
|
9074
9361
|
function isExplicitChannelPostIntent(text) {
|
|
9075
9362
|
if (!/\bchannel\b/i.test(text)) {
|
|
9076
9363
|
return false;
|
|
@@ -9372,6 +9659,50 @@ async function persistAuthPauseCheckpoint(args) {
|
|
|
9372
9659
|
}
|
|
9373
9660
|
return nextSliceId;
|
|
9374
9661
|
}
|
|
9662
|
+
async function persistTimeoutCheckpoint(args) {
|
|
9663
|
+
const nextSliceId = args.currentSliceId + 1;
|
|
9664
|
+
try {
|
|
9665
|
+
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
9666
|
+
args.conversationId,
|
|
9667
|
+
args.sessionId
|
|
9668
|
+
);
|
|
9669
|
+
const piMessages = trimTrailingAssistantMessages(
|
|
9670
|
+
args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
|
|
9671
|
+
);
|
|
9672
|
+
return await upsertAgentTurnSessionCheckpoint({
|
|
9673
|
+
conversationId: args.conversationId,
|
|
9674
|
+
sessionId: args.sessionId,
|
|
9675
|
+
sliceId: nextSliceId,
|
|
9676
|
+
state: "awaiting_resume",
|
|
9677
|
+
piMessages,
|
|
9678
|
+
loadedSkillNames: args.loadedSkillNames,
|
|
9679
|
+
resumeReason: "timeout",
|
|
9680
|
+
resumedFromSliceId: args.currentSliceId,
|
|
9681
|
+
errorMessage: args.errorMessage
|
|
9682
|
+
});
|
|
9683
|
+
} catch (checkpointError) {
|
|
9684
|
+
logException(
|
|
9685
|
+
checkpointError,
|
|
9686
|
+
"agent_turn_timeout_resume_checkpoint_failed",
|
|
9687
|
+
{
|
|
9688
|
+
slackThreadId: args.logContext.threadId,
|
|
9689
|
+
slackUserId: args.logContext.requesterId,
|
|
9690
|
+
slackChannelId: args.logContext.channelId,
|
|
9691
|
+
runId: args.logContext.runId,
|
|
9692
|
+
assistantUserName: args.logContext.assistantUserName,
|
|
9693
|
+
modelId: args.logContext.modelId
|
|
9694
|
+
},
|
|
9695
|
+
{
|
|
9696
|
+
"app.ai.resume_conversation_id": args.conversationId,
|
|
9697
|
+
"app.ai.resume_session_id": args.sessionId,
|
|
9698
|
+
"app.ai.resume_from_slice_id": args.currentSliceId,
|
|
9699
|
+
"app.ai.resume_next_slice_id": nextSliceId
|
|
9700
|
+
},
|
|
9701
|
+
"Failed to persist timeout checkpoint before scheduling resume"
|
|
9702
|
+
);
|
|
9703
|
+
return void 0;
|
|
9704
|
+
}
|
|
9705
|
+
}
|
|
9375
9706
|
|
|
9376
9707
|
// src/chat/services/mcp-auth-orchestration.ts
|
|
9377
9708
|
var McpAuthorizationPauseError = class extends Error {
|
|
@@ -9462,21 +9793,6 @@ function mcpToolsToDefinitions(mcpTools) {
|
|
|
9462
9793
|
}
|
|
9463
9794
|
return defs;
|
|
9464
9795
|
}
|
|
9465
|
-
async function maybeReplaceAgentMessages(agent, messages) {
|
|
9466
|
-
const resumable = agent;
|
|
9467
|
-
if (typeof resumable.replaceMessages !== "function") {
|
|
9468
|
-
return false;
|
|
9469
|
-
}
|
|
9470
|
-
await resumable.replaceMessages(messages);
|
|
9471
|
-
return true;
|
|
9472
|
-
}
|
|
9473
|
-
async function runAgentContinuation(agent) {
|
|
9474
|
-
const resumable = agent;
|
|
9475
|
-
if (typeof resumable.continue !== "function") {
|
|
9476
|
-
throw new Error("Agent continuation is unavailable in this runtime");
|
|
9477
|
-
}
|
|
9478
|
-
return await resumable.continue();
|
|
9479
|
-
}
|
|
9480
9796
|
async function generateAssistantReply(messageText, context = {}) {
|
|
9481
9797
|
let timeoutResumeConversationId;
|
|
9482
9798
|
let timeoutResumeSessionId;
|
|
@@ -9487,6 +9803,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9487
9803
|
let loadedSkillNamesForResume = [];
|
|
9488
9804
|
let mcpToolManager;
|
|
9489
9805
|
let sandboxExecutor;
|
|
9806
|
+
let timedOut = false;
|
|
9490
9807
|
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
9491
9808
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
9492
9809
|
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
@@ -9529,9 +9846,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9529
9846
|
"Discovered startup SOUL/skills/plugins"
|
|
9530
9847
|
);
|
|
9531
9848
|
}
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
};
|
|
9849
|
+
let baseInstructions = "";
|
|
9850
|
+
let configurationValues;
|
|
9535
9851
|
const userInput = messageText;
|
|
9536
9852
|
if (shouldTrace) {
|
|
9537
9853
|
logInfo(
|
|
@@ -9560,6 +9876,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9560
9876
|
timeoutResumeConversationId = sessionConversationId;
|
|
9561
9877
|
timeoutResumeSessionId = sessionId;
|
|
9562
9878
|
timeoutResumeSliceId = currentSliceId;
|
|
9879
|
+
const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
|
|
9880
|
+
configurationValues = {
|
|
9881
|
+
...context.configuration ?? {},
|
|
9882
|
+
...persistedConfigurationValues
|
|
9883
|
+
};
|
|
9563
9884
|
const capabilityRuntime = createSkillCapabilityRuntime({
|
|
9564
9885
|
invocationArgs: skillInvocation?.args,
|
|
9565
9886
|
requesterId: context.requester?.userId,
|
|
@@ -9571,6 +9892,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9571
9892
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
9572
9893
|
traceContext: spanContext,
|
|
9573
9894
|
onStatus: context.onStatus,
|
|
9895
|
+
onSandboxAcquired: async (sandbox2) => {
|
|
9896
|
+
lastKnownSandboxId = sandbox2.sandboxId;
|
|
9897
|
+
lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
|
|
9898
|
+
await context.onSandboxAcquired?.(sandbox2);
|
|
9899
|
+
},
|
|
9574
9900
|
runBashCustomCommand: async (command) => {
|
|
9575
9901
|
const result = await maybeExecuteJrRpcCustomCommand(command, {
|
|
9576
9902
|
capabilityRuntime,
|
|
@@ -9710,8 +10036,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9710
10036
|
onGeneratedFiles: (files) => {
|
|
9711
10037
|
replyFiles.push(...files);
|
|
9712
10038
|
},
|
|
9713
|
-
onArtifactStatePatch: (patch) => {
|
|
10039
|
+
onArtifactStatePatch: async (patch) => {
|
|
9714
10040
|
Object.assign(artifactStatePatch, patch);
|
|
10041
|
+
await context.onArtifactStateUpdated?.(
|
|
10042
|
+
mergeArtifactsState(
|
|
10043
|
+
context.artifactState ?? {},
|
|
10044
|
+
artifactStatePatch
|
|
10045
|
+
)
|
|
10046
|
+
);
|
|
9715
10047
|
},
|
|
9716
10048
|
toolOverrides: context.toolOverrides,
|
|
9717
10049
|
onSkillLoaded: async (loadedSkill) => {
|
|
@@ -9761,7 +10093,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9761
10093
|
}
|
|
9762
10094
|
syncResumeState();
|
|
9763
10095
|
const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
|
|
9764
|
-
|
|
10096
|
+
baseInstructions = buildSystemPrompt({
|
|
9765
10097
|
availableSkills,
|
|
9766
10098
|
activeSkills,
|
|
9767
10099
|
activeTools: activeToolSummaries,
|
|
@@ -9821,6 +10153,17 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9821
10153
|
const agentToolHooks = {
|
|
9822
10154
|
onToolCall: (toolName) => {
|
|
9823
10155
|
toolCalls.push(toolName);
|
|
10156
|
+
Promise.resolve(context.onToolCall?.(toolName)).catch((error) => {
|
|
10157
|
+
logWarn(
|
|
10158
|
+
"streaming_tool_call_error",
|
|
10159
|
+
{},
|
|
10160
|
+
{
|
|
10161
|
+
"error.message": error instanceof Error ? error.message : String(error),
|
|
10162
|
+
"gen_ai.tool.name": toolName
|
|
10163
|
+
},
|
|
10164
|
+
"Failed to deliver tool call event to stream coordinator"
|
|
10165
|
+
);
|
|
10166
|
+
});
|
|
9824
10167
|
}
|
|
9825
10168
|
};
|
|
9826
10169
|
const baseAgentTools = createAgentTools(
|
|
@@ -9861,6 +10204,16 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9861
10204
|
let needsSeparator = false;
|
|
9862
10205
|
const unsubscribe = agent.subscribe((event) => {
|
|
9863
10206
|
if (event.type === "message_start") {
|
|
10207
|
+
Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
|
|
10208
|
+
logWarn(
|
|
10209
|
+
"streaming_message_start_error",
|
|
10210
|
+
{},
|
|
10211
|
+
{
|
|
10212
|
+
"error.message": error instanceof Error ? error.message : String(error)
|
|
10213
|
+
},
|
|
10214
|
+
"Failed to deliver assistant message start to stream coordinator"
|
|
10215
|
+
);
|
|
10216
|
+
});
|
|
9864
10217
|
if (hasEmittedText) {
|
|
9865
10218
|
needsSeparator = true;
|
|
9866
10219
|
}
|
|
@@ -9889,15 +10242,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9889
10242
|
let completedAssistantTurn = false;
|
|
9890
10243
|
try {
|
|
9891
10244
|
if (resumedFromCheckpoint) {
|
|
9892
|
-
|
|
9893
|
-
agent,
|
|
9894
|
-
existingCheckpoint.piMessages
|
|
9895
|
-
);
|
|
9896
|
-
if (!didReplace) {
|
|
9897
|
-
throw new Error(
|
|
9898
|
-
"Agent session resume requested but replaceMessages is unavailable"
|
|
9899
|
-
);
|
|
9900
|
-
}
|
|
10245
|
+
agent.replaceMessages(existingCheckpoint.piMessages);
|
|
9901
10246
|
}
|
|
9902
10247
|
beforeMessageCount = agent.state.messages.length;
|
|
9903
10248
|
await withSpan(
|
|
@@ -9906,16 +10251,15 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9906
10251
|
spanContext,
|
|
9907
10252
|
async () => {
|
|
9908
10253
|
let promptResult;
|
|
9909
|
-
const promptPromise = resumedFromCheckpoint ?
|
|
10254
|
+
const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
|
|
9910
10255
|
role: "user",
|
|
9911
10256
|
content: userContentParts,
|
|
9912
10257
|
timestamp: Date.now()
|
|
9913
10258
|
});
|
|
9914
10259
|
let timeoutId;
|
|
9915
|
-
let didTimeout = false;
|
|
9916
10260
|
const timeoutPromise = new Promise((_, reject) => {
|
|
9917
10261
|
timeoutId = setTimeout(() => {
|
|
9918
|
-
|
|
10262
|
+
timedOut = true;
|
|
9919
10263
|
agent.abort();
|
|
9920
10264
|
reject(
|
|
9921
10265
|
new Error(
|
|
@@ -9927,7 +10271,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9927
10271
|
try {
|
|
9928
10272
|
promptResult = await Promise.race([promptPromise, timeoutPromise]);
|
|
9929
10273
|
} catch (error) {
|
|
9930
|
-
if (
|
|
10274
|
+
if (timedOut) {
|
|
9931
10275
|
logWarn(
|
|
9932
10276
|
"agent_turn_timeout",
|
|
9933
10277
|
{},
|
|
@@ -9953,9 +10297,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9953
10297
|
clearTimeout(timeoutId);
|
|
9954
10298
|
}
|
|
9955
10299
|
}
|
|
9956
|
-
newMessages = agent.state.messages.slice(
|
|
9957
|
-
beforeMessageCount
|
|
9958
|
-
);
|
|
10300
|
+
newMessages = agent.state.messages.slice(beforeMessageCount);
|
|
9959
10301
|
completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
|
|
9960
10302
|
if (mcpAuth.getPendingPause() && !completedAssistantTurn) {
|
|
9961
10303
|
timeoutResumeMessages = [...agent.state.messages];
|
|
@@ -10011,6 +10353,36 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10011
10353
|
assistantUserName: context.assistant?.userName
|
|
10012
10354
|
});
|
|
10013
10355
|
} catch (error) {
|
|
10356
|
+
if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
10357
|
+
const checkpoint = await persistTimeoutCheckpoint({
|
|
10358
|
+
conversationId: timeoutResumeConversationId,
|
|
10359
|
+
sessionId: timeoutResumeSessionId,
|
|
10360
|
+
currentSliceId: timeoutResumeSliceId,
|
|
10361
|
+
messages: timeoutResumeMessages,
|
|
10362
|
+
loadedSkillNames: loadedSkillNamesForResume,
|
|
10363
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
10364
|
+
logContext: {
|
|
10365
|
+
threadId: context.correlation?.threadId,
|
|
10366
|
+
requesterId: context.correlation?.requesterId,
|
|
10367
|
+
channelId: context.correlation?.channelId,
|
|
10368
|
+
runId: context.correlation?.runId,
|
|
10369
|
+
assistantUserName: context.assistant?.userName,
|
|
10370
|
+
modelId: botConfig.modelId
|
|
10371
|
+
}
|
|
10372
|
+
});
|
|
10373
|
+
if (checkpoint) {
|
|
10374
|
+
throw new RetryableTurnError(
|
|
10375
|
+
"turn_timeout_resume",
|
|
10376
|
+
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId} version=${checkpoint.checkpointVersion}`,
|
|
10377
|
+
{
|
|
10378
|
+
conversationId: timeoutResumeConversationId,
|
|
10379
|
+
sessionId: timeoutResumeSessionId,
|
|
10380
|
+
sliceId: checkpoint.sliceId,
|
|
10381
|
+
checkpointVersion: checkpoint.checkpointVersion
|
|
10382
|
+
}
|
|
10383
|
+
);
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10014
10386
|
if (error instanceof McpAuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
10015
10387
|
const nextSliceId = await persistAuthPauseCheckpoint({
|
|
10016
10388
|
conversationId: timeoutResumeConversationId,
|
|
@@ -10030,7 +10402,12 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
10030
10402
|
});
|
|
10031
10403
|
throw new RetryableTurnError(
|
|
10032
10404
|
"mcp_auth_resume",
|
|
10033
|
-
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}
|
|
10405
|
+
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
|
|
10406
|
+
{
|
|
10407
|
+
conversationId: timeoutResumeConversationId,
|
|
10408
|
+
sessionId: timeoutResumeSessionId,
|
|
10409
|
+
sliceId: nextSliceId
|
|
10410
|
+
}
|
|
10034
10411
|
);
|
|
10035
10412
|
}
|
|
10036
10413
|
if (isRetryableTurnError(error)) {
|
|
@@ -10228,7 +10605,194 @@ function createProgressReporter(args) {
|
|
|
10228
10605
|
};
|
|
10229
10606
|
}
|
|
10230
10607
|
|
|
10231
|
-
// src/
|
|
10608
|
+
// src/chat/slack/reply.ts
|
|
10609
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
10610
|
+
function isInterruptedVisibleReply(reply) {
|
|
10611
|
+
return reply.diagnostics.outcome === "provider_error";
|
|
10612
|
+
}
|
|
10613
|
+
function buildChunkMessage(chunk, files) {
|
|
10614
|
+
return {
|
|
10615
|
+
markdown: chunk,
|
|
10616
|
+
...files ? { files } : {}
|
|
10617
|
+
};
|
|
10618
|
+
}
|
|
10619
|
+
function buildTextPosts(args) {
|
|
10620
|
+
const chunks = splitSlackReplyText(args.text, {
|
|
10621
|
+
interrupted: args.interrupted
|
|
10622
|
+
});
|
|
10623
|
+
return chunks.map((chunk, index) => ({
|
|
10624
|
+
message: buildChunkMessage(
|
|
10625
|
+
chunk,
|
|
10626
|
+
index === 0 ? args.firstFiles : void 0
|
|
10627
|
+
),
|
|
10628
|
+
stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
|
|
10629
|
+
}));
|
|
10630
|
+
}
|
|
10631
|
+
async function normalizeFileUploads(files) {
|
|
10632
|
+
return await Promise.all(
|
|
10633
|
+
files.map(async (file) => {
|
|
10634
|
+
let data;
|
|
10635
|
+
if (Buffer2.isBuffer(file.data)) {
|
|
10636
|
+
data = file.data;
|
|
10637
|
+
} else if (file.data instanceof ArrayBuffer) {
|
|
10638
|
+
data = Buffer2.from(file.data);
|
|
10639
|
+
} else {
|
|
10640
|
+
data = Buffer2.from(await file.data.arrayBuffer());
|
|
10641
|
+
}
|
|
10642
|
+
return {
|
|
10643
|
+
data,
|
|
10644
|
+
filename: file.filename
|
|
10645
|
+
};
|
|
10646
|
+
})
|
|
10647
|
+
);
|
|
10648
|
+
}
|
|
10649
|
+
async function uploadReplyFilesBestEffort(args) {
|
|
10650
|
+
try {
|
|
10651
|
+
await uploadFilesToThread({
|
|
10652
|
+
channelId: args.channelId,
|
|
10653
|
+
threadTs: args.threadTs,
|
|
10654
|
+
files: await normalizeFileUploads(args.files)
|
|
10655
|
+
});
|
|
10656
|
+
} catch {
|
|
10657
|
+
}
|
|
10658
|
+
}
|
|
10659
|
+
function getReplyMessageText(message) {
|
|
10660
|
+
if (typeof message !== "object" || message === null) {
|
|
10661
|
+
return void 0;
|
|
10662
|
+
}
|
|
10663
|
+
if ("markdown" in message && typeof message.markdown === "string") {
|
|
10664
|
+
return message.markdown;
|
|
10665
|
+
}
|
|
10666
|
+
if ("raw" in message && typeof message.raw === "string") {
|
|
10667
|
+
return message.raw;
|
|
10668
|
+
}
|
|
10669
|
+
return void 0;
|
|
10670
|
+
}
|
|
10671
|
+
function getReplyMessageFiles(message) {
|
|
10672
|
+
if (typeof message === "object" && message !== null && "files" in message && Array.isArray(message.files)) {
|
|
10673
|
+
return message.files;
|
|
10674
|
+
}
|
|
10675
|
+
return void 0;
|
|
10676
|
+
}
|
|
10677
|
+
function createSlackStreamAccumulator() {
|
|
10678
|
+
let pendingCarriageReturn = false;
|
|
10679
|
+
let streamedVisibleText = "";
|
|
10680
|
+
let streamedRenderedText = "";
|
|
10681
|
+
let overflowText = "";
|
|
10682
|
+
let streamOverflowed = false;
|
|
10683
|
+
const continuationBudget = getSlackStreamingContinuationBudget();
|
|
10684
|
+
const normalizeDelta = (deltaText) => {
|
|
10685
|
+
let text = deltaText;
|
|
10686
|
+
if (pendingCarriageReturn) {
|
|
10687
|
+
text = `\r${text}`;
|
|
10688
|
+
pendingCarriageReturn = false;
|
|
10689
|
+
}
|
|
10690
|
+
if (text.endsWith("\r")) {
|
|
10691
|
+
text = text.slice(0, -1);
|
|
10692
|
+
pendingCarriageReturn = true;
|
|
10693
|
+
}
|
|
10694
|
+
return text.replace(/\r\n?/g, "\n");
|
|
10695
|
+
};
|
|
10696
|
+
return {
|
|
10697
|
+
append(deltaText) {
|
|
10698
|
+
const normalizedDeltaText = normalizeDelta(deltaText);
|
|
10699
|
+
if (!normalizedDeltaText) {
|
|
10700
|
+
return "";
|
|
10701
|
+
}
|
|
10702
|
+
if (streamOverflowed) {
|
|
10703
|
+
overflowText += normalizedDeltaText;
|
|
10704
|
+
return "";
|
|
10705
|
+
}
|
|
10706
|
+
const candidate = `${streamedVisibleText}${normalizedDeltaText}`;
|
|
10707
|
+
const { prefix, renderedPrefix, rest } = takeSlackContinuationPrefix(
|
|
10708
|
+
candidate,
|
|
10709
|
+
continuationBudget
|
|
10710
|
+
);
|
|
10711
|
+
const additional = renderedPrefix.length > streamedRenderedText.length ? renderedPrefix.slice(streamedRenderedText.length) : "";
|
|
10712
|
+
streamedVisibleText = prefix;
|
|
10713
|
+
streamedRenderedText = renderedPrefix;
|
|
10714
|
+
if (rest) {
|
|
10715
|
+
overflowText += rest;
|
|
10716
|
+
streamOverflowed = true;
|
|
10717
|
+
}
|
|
10718
|
+
return additional;
|
|
10719
|
+
},
|
|
10720
|
+
getOverflowText() {
|
|
10721
|
+
return overflowText;
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
}
|
|
10725
|
+
function planSlackReplyPosts(args) {
|
|
10726
|
+
const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
|
|
10727
|
+
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
10728
|
+
reply: args.reply,
|
|
10729
|
+
hasStreamedThreadReply: args.hasStreamedThreadReply
|
|
10730
|
+
});
|
|
10731
|
+
const interrupted = isInterruptedVisibleReply(args.reply);
|
|
10732
|
+
const posts = [];
|
|
10733
|
+
if (args.hasStreamedThreadReply) {
|
|
10734
|
+
if (shouldPostThreadReply && args.streamedOverflowText) {
|
|
10735
|
+
posts.push(
|
|
10736
|
+
...buildTextPosts({
|
|
10737
|
+
text: args.streamedOverflowText,
|
|
10738
|
+
interrupted,
|
|
10739
|
+
firstStage: "thread_reply_continuation"
|
|
10740
|
+
})
|
|
10741
|
+
);
|
|
10742
|
+
} else if (shouldPostThreadReply && interrupted) {
|
|
10743
|
+
posts.push({
|
|
10744
|
+
message: buildSlackOutputMessage(
|
|
10745
|
+
getSlackInterruptionMarker().trimStart()
|
|
10746
|
+
),
|
|
10747
|
+
stage: "thread_reply_continuation"
|
|
10748
|
+
});
|
|
10749
|
+
}
|
|
10750
|
+
} else {
|
|
10751
|
+
const textPosts = shouldPostThreadReply ? buildTextPosts({
|
|
10752
|
+
text: args.reply.text,
|
|
10753
|
+
interrupted,
|
|
10754
|
+
firstFiles: attachFiles === "inline" ? replyFiles : void 0
|
|
10755
|
+
}) : [];
|
|
10756
|
+
posts.push(...textPosts);
|
|
10757
|
+
if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
|
|
10758
|
+
posts.push({
|
|
10759
|
+
message: buildSlackOutputMessage("", replyFiles),
|
|
10760
|
+
stage: "thread_reply"
|
|
10761
|
+
});
|
|
10762
|
+
} else if (shouldPostThreadReply && textPosts.length === 0) {
|
|
10763
|
+
posts.push({
|
|
10764
|
+
message: buildSlackOutputMessage(args.reply.text),
|
|
10765
|
+
stage: "thread_reply"
|
|
10766
|
+
});
|
|
10767
|
+
}
|
|
10768
|
+
}
|
|
10769
|
+
if (attachFiles === "followup" && replyFiles) {
|
|
10770
|
+
posts.push({
|
|
10771
|
+
message: buildSlackOutputMessage("", replyFiles),
|
|
10772
|
+
stage: "thread_reply_files_followup"
|
|
10773
|
+
});
|
|
10774
|
+
}
|
|
10775
|
+
return posts;
|
|
10776
|
+
}
|
|
10777
|
+
async function postSlackApiReplyPosts(args) {
|
|
10778
|
+
for (const post of args.posts) {
|
|
10779
|
+
const text = getReplyMessageText(post.message);
|
|
10780
|
+
if (text && text.trim().length > 0) {
|
|
10781
|
+
await args.postMessage(args.channelId, args.threadTs, text);
|
|
10782
|
+
}
|
|
10783
|
+
const files = getReplyMessageFiles(post.message);
|
|
10784
|
+
if (!files?.length) {
|
|
10785
|
+
continue;
|
|
10786
|
+
}
|
|
10787
|
+
await uploadReplyFilesBestEffort({
|
|
10788
|
+
channelId: args.channelId,
|
|
10789
|
+
threadTs: args.threadTs,
|
|
10790
|
+
files
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
|
|
10795
|
+
// src/chat/slack/resume.ts
|
|
10232
10796
|
function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
10233
10797
|
if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
|
|
10234
10798
|
return explicitTimeoutMs;
|
|
@@ -10241,12 +10805,15 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
|
10241
10805
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
10242
10806
|
}
|
|
10243
10807
|
async function postSlackMessage(channelId, threadTs, text) {
|
|
10808
|
+
await getSlackClient().chat.postMessage({
|
|
10809
|
+
channel: channelId,
|
|
10810
|
+
thread_ts: threadTs,
|
|
10811
|
+
text
|
|
10812
|
+
});
|
|
10813
|
+
}
|
|
10814
|
+
async function postSlackMessageBestEffort(channelId, threadTs, text) {
|
|
10244
10815
|
try {
|
|
10245
|
-
await
|
|
10246
|
-
channel: channelId,
|
|
10247
|
-
thread_ts: threadTs,
|
|
10248
|
-
text
|
|
10249
|
-
});
|
|
10816
|
+
await postSlackMessage(channelId, threadTs, text);
|
|
10250
10817
|
} catch {
|
|
10251
10818
|
}
|
|
10252
10819
|
}
|
|
@@ -10276,32 +10843,85 @@ function createReadOnlyConfigService(values) {
|
|
|
10276
10843
|
}
|
|
10277
10844
|
};
|
|
10278
10845
|
}
|
|
10279
|
-
|
|
10846
|
+
var ResumeTurnBusyError = class extends Error {
|
|
10847
|
+
constructor(lockKey) {
|
|
10848
|
+
super(`A turn already owns resume lock "${lockKey}"`);
|
|
10849
|
+
this.name = "ResumeTurnBusyError";
|
|
10850
|
+
}
|
|
10851
|
+
};
|
|
10852
|
+
function getDefaultLockKey(channelId, threadTs) {
|
|
10853
|
+
return `slack:${channelId}:${threadTs}`;
|
|
10854
|
+
}
|
|
10855
|
+
function createResumeReplyContext(args, progress) {
|
|
10856
|
+
const replyContext = args.replyContext ?? {};
|
|
10857
|
+
const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
10858
|
+
const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
|
|
10859
|
+
return {
|
|
10860
|
+
...replyContext,
|
|
10861
|
+
assistant: {
|
|
10862
|
+
userName: botConfig.userName,
|
|
10863
|
+
...replyContext.assistant
|
|
10864
|
+
},
|
|
10865
|
+
correlation: {
|
|
10866
|
+
...replyContext.correlation,
|
|
10867
|
+
threadId: replyContext.correlation?.threadId ?? threadId,
|
|
10868
|
+
channelId: replyContext.correlation?.channelId ?? args.channelId,
|
|
10869
|
+
threadTs: replyContext.correlation?.threadTs ?? args.threadTs,
|
|
10870
|
+
requesterId: replyContext.correlation?.requesterId ?? replyContext.requester?.userId
|
|
10871
|
+
},
|
|
10872
|
+
channelConfiguration: persistedChannelConfiguration,
|
|
10873
|
+
onSandboxAcquired: async (sandbox) => {
|
|
10874
|
+
await persistThreadStateById(threadId, {
|
|
10875
|
+
sandboxId: sandbox.sandboxId,
|
|
10876
|
+
sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
|
|
10877
|
+
});
|
|
10878
|
+
await replyContext.onSandboxAcquired?.(sandbox);
|
|
10879
|
+
},
|
|
10880
|
+
onArtifactStateUpdated: async (artifacts) => {
|
|
10881
|
+
await persistThreadStateById(threadId, { artifacts });
|
|
10882
|
+
await replyContext.onArtifactStateUpdated?.(artifacts);
|
|
10883
|
+
},
|
|
10884
|
+
onStatus: async (status) => {
|
|
10885
|
+
await progress.setStatus(status);
|
|
10886
|
+
await replyContext.onStatus?.(status);
|
|
10887
|
+
}
|
|
10888
|
+
};
|
|
10889
|
+
}
|
|
10890
|
+
async function resumeSlackTurn(args) {
|
|
10891
|
+
const requesterUserId = args.replyContext?.requester?.userId;
|
|
10892
|
+
if (!requesterUserId) {
|
|
10893
|
+
throw new Error("Resumed turn requires replyContext.requester.userId");
|
|
10894
|
+
}
|
|
10895
|
+
const stateAdapter = getStateAdapter();
|
|
10896
|
+
await stateAdapter.connect();
|
|
10897
|
+
const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
10898
|
+
const lock = await stateAdapter.acquireLock(
|
|
10899
|
+
lockKey,
|
|
10900
|
+
botConfig.turnTimeoutMs + 6e4
|
|
10901
|
+
);
|
|
10902
|
+
if (!lock) {
|
|
10903
|
+
throw new ResumeTurnBusyError(lockKey);
|
|
10904
|
+
}
|
|
10280
10905
|
const progress = createProgressReporter({
|
|
10281
10906
|
channelId: args.channelId,
|
|
10282
10907
|
threadTs: args.threadTs,
|
|
10283
10908
|
transport: createSlackWebApiAssistantStatusTransport()
|
|
10284
10909
|
});
|
|
10285
|
-
|
|
10286
|
-
|
|
10910
|
+
let deferredPauseHandler;
|
|
10911
|
+
let deferredFailureHandler;
|
|
10287
10912
|
try {
|
|
10913
|
+
if (args.initialText) {
|
|
10914
|
+
await postSlackMessageBestEffort(
|
|
10915
|
+
args.channelId,
|
|
10916
|
+
args.threadTs,
|
|
10917
|
+
args.initialText
|
|
10918
|
+
);
|
|
10919
|
+
}
|
|
10920
|
+
await progress.start();
|
|
10288
10921
|
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
10922
|
+
const replyContext = createResumeReplyContext(args, progress);
|
|
10289
10923
|
const replyPromise = generateReply(args.messageText, {
|
|
10290
|
-
|
|
10291
|
-
requester: { userId: args.requesterUserId },
|
|
10292
|
-
correlation: {
|
|
10293
|
-
conversationId: args.correlation?.conversationId,
|
|
10294
|
-
turnId: args.correlation?.turnId,
|
|
10295
|
-
channelId: args.correlation?.channelId ?? args.channelId,
|
|
10296
|
-
threadTs: args.correlation?.threadTs ?? args.threadTs,
|
|
10297
|
-
requesterId: args.correlation?.requesterId ?? args.requesterUserId
|
|
10298
|
-
},
|
|
10299
|
-
toolChannelId: args.toolChannelId,
|
|
10300
|
-
conversationContext: args.conversationContext,
|
|
10301
|
-
artifactState: args.artifactState,
|
|
10302
|
-
configuration: args.configuration,
|
|
10303
|
-
channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
|
|
10304
|
-
onStatus: (status) => progress.setStatus(status)
|
|
10924
|
+
...replyContext
|
|
10305
10925
|
});
|
|
10306
10926
|
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
10307
10927
|
const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
@@ -10318,20 +10938,180 @@ async function resumeAuthorizedRequest(args) {
|
|
|
10318
10938
|
)
|
|
10319
10939
|
]) : await replyPromise;
|
|
10320
10940
|
await progress.stop();
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10941
|
+
await postSlackApiReplyPosts({
|
|
10942
|
+
channelId: args.channelId,
|
|
10943
|
+
threadTs: args.threadTs,
|
|
10944
|
+
posts: planSlackReplyPosts({
|
|
10945
|
+
reply,
|
|
10946
|
+
hasStreamedThreadReply: false
|
|
10947
|
+
}),
|
|
10948
|
+
postMessage: postSlackMessage
|
|
10949
|
+
});
|
|
10326
10950
|
await args.onSuccess?.(reply);
|
|
10327
10951
|
} catch (error) {
|
|
10328
10952
|
await progress.stop();
|
|
10329
10953
|
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
10330
|
-
|
|
10954
|
+
deferredPauseHandler = async () => {
|
|
10955
|
+
await args.onAuthPause?.(error);
|
|
10956
|
+
};
|
|
10957
|
+
} else if (isRetryableTurnError(error, "turn_timeout_resume") && args.onTimeoutPause) {
|
|
10958
|
+
deferredPauseHandler = async () => {
|
|
10959
|
+
await args.onTimeoutPause?.(error);
|
|
10960
|
+
};
|
|
10961
|
+
} else {
|
|
10962
|
+
deferredFailureHandler = async () => {
|
|
10963
|
+
await args.onFailure?.(error);
|
|
10964
|
+
if (args.failureText) {
|
|
10965
|
+
await postSlackMessageBestEffort(
|
|
10966
|
+
args.channelId,
|
|
10967
|
+
args.threadTs,
|
|
10968
|
+
args.failureText
|
|
10969
|
+
);
|
|
10970
|
+
}
|
|
10971
|
+
};
|
|
10972
|
+
}
|
|
10973
|
+
} finally {
|
|
10974
|
+
await stateAdapter.releaseLock(lock);
|
|
10975
|
+
}
|
|
10976
|
+
if (deferredPauseHandler) {
|
|
10977
|
+
try {
|
|
10978
|
+
await deferredPauseHandler();
|
|
10979
|
+
return;
|
|
10980
|
+
} catch (pauseError) {
|
|
10981
|
+
await args.onFailure?.(pauseError);
|
|
10982
|
+
if (args.failureText) {
|
|
10983
|
+
await postSlackMessageBestEffort(
|
|
10984
|
+
args.channelId,
|
|
10985
|
+
args.threadTs,
|
|
10986
|
+
args.failureText
|
|
10987
|
+
);
|
|
10988
|
+
}
|
|
10331
10989
|
return;
|
|
10332
10990
|
}
|
|
10333
|
-
|
|
10334
|
-
|
|
10991
|
+
}
|
|
10992
|
+
if (deferredFailureHandler) {
|
|
10993
|
+
await deferredFailureHandler();
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
async function resumeAuthorizedRequest(args) {
|
|
10997
|
+
await resumeSlackTurn({
|
|
10998
|
+
messageText: args.messageText,
|
|
10999
|
+
channelId: args.channelId,
|
|
11000
|
+
threadTs: args.threadTs,
|
|
11001
|
+
replyContext: args.replyContext,
|
|
11002
|
+
lockKey: args.lockKey,
|
|
11003
|
+
initialText: args.connectedText,
|
|
11004
|
+
failureText: args.failureText,
|
|
11005
|
+
generateReply: args.generateReply,
|
|
11006
|
+
onSuccess: args.onSuccess,
|
|
11007
|
+
onFailure: args.onFailure,
|
|
11008
|
+
onAuthPause: args.onAuthPause,
|
|
11009
|
+
onTimeoutPause: args.onTimeoutPause,
|
|
11010
|
+
replyTimeoutMs: args.replyTimeoutMs
|
|
11011
|
+
});
|
|
11012
|
+
}
|
|
11013
|
+
|
|
11014
|
+
// src/chat/services/timeout-resume.ts
|
|
11015
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
11016
|
+
var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
|
|
11017
|
+
var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
|
|
11018
|
+
var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
|
|
11019
|
+
var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
|
|
11020
|
+
var TURN_TIMEOUT_RESUME_SIGNATURE_HEADER = "x-junior-resume-signature";
|
|
11021
|
+
var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
|
|
11022
|
+
function canScheduleTurnTimeoutResume(nextSliceId) {
|
|
11023
|
+
return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
|
|
11024
|
+
}
|
|
11025
|
+
function getTurnTimeoutResumeSecret() {
|
|
11026
|
+
const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
|
|
11027
|
+
if (explicit) {
|
|
11028
|
+
return explicit;
|
|
11029
|
+
}
|
|
11030
|
+
return getSlackSigningSecret();
|
|
11031
|
+
}
|
|
11032
|
+
function buildSignedPayload(timestamp, body) {
|
|
11033
|
+
return `${timestamp}:${body}`;
|
|
11034
|
+
}
|
|
11035
|
+
function signTurnTimeoutResumeBody(secret, timestamp, body) {
|
|
11036
|
+
const digest = createHmac("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
|
|
11037
|
+
return `${TURN_TIMEOUT_RESUME_SIGNATURE_VERSION}=${digest}`;
|
|
11038
|
+
}
|
|
11039
|
+
function timingSafeMatch(expected, actual) {
|
|
11040
|
+
const expectedBuffer = Buffer.from(expected);
|
|
11041
|
+
const actualBuffer = Buffer.from(actual);
|
|
11042
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
11043
|
+
return false;
|
|
11044
|
+
}
|
|
11045
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
11046
|
+
}
|
|
11047
|
+
function parseTurnTimeoutResumeRequest(value) {
|
|
11048
|
+
if (!value || typeof value !== "object") {
|
|
11049
|
+
return void 0;
|
|
11050
|
+
}
|
|
11051
|
+
const record = value;
|
|
11052
|
+
if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof record.expectedCheckpointVersion !== "number") {
|
|
11053
|
+
return void 0;
|
|
11054
|
+
}
|
|
11055
|
+
return {
|
|
11056
|
+
conversationId: record.conversationId,
|
|
11057
|
+
sessionId: record.sessionId,
|
|
11058
|
+
expectedCheckpointVersion: record.expectedCheckpointVersion
|
|
11059
|
+
};
|
|
11060
|
+
}
|
|
11061
|
+
async function scheduleTurnTimeoutResume(request) {
|
|
11062
|
+
const baseUrl = resolveBaseUrl();
|
|
11063
|
+
if (!baseUrl) {
|
|
11064
|
+
throw new Error(
|
|
11065
|
+
"Cannot determine base URL for timeout resume callback (set JUNIOR_BASE_URL or deploy to Vercel)"
|
|
11066
|
+
);
|
|
11067
|
+
}
|
|
11068
|
+
const secret = getTurnTimeoutResumeSecret();
|
|
11069
|
+
if (!secret) {
|
|
11070
|
+
throw new Error(
|
|
11071
|
+
"Cannot determine timeout resume secret (set JUNIOR_INTERNAL_RESUME_SECRET or SLACK_SIGNING_SECRET)"
|
|
11072
|
+
);
|
|
11073
|
+
}
|
|
11074
|
+
const body = JSON.stringify(request);
|
|
11075
|
+
const timestamp = Date.now().toString();
|
|
11076
|
+
const response = await fetch(`${baseUrl}${TURN_TIMEOUT_RESUME_PATH}`, {
|
|
11077
|
+
method: "POST",
|
|
11078
|
+
headers: {
|
|
11079
|
+
"content-type": "application/json",
|
|
11080
|
+
[TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER]: timestamp,
|
|
11081
|
+
[TURN_TIMEOUT_RESUME_SIGNATURE_HEADER]: signTurnTimeoutResumeBody(
|
|
11082
|
+
secret,
|
|
11083
|
+
timestamp,
|
|
11084
|
+
body
|
|
11085
|
+
)
|
|
11086
|
+
},
|
|
11087
|
+
body
|
|
11088
|
+
});
|
|
11089
|
+
if (!response.ok) {
|
|
11090
|
+
throw new Error(
|
|
11091
|
+
`Timeout resume callback failed with status ${response.status}`
|
|
11092
|
+
);
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
async function verifyTurnTimeoutResumeRequest(request) {
|
|
11096
|
+
const timestamp = request.headers.get(TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER)?.trim() ?? "";
|
|
11097
|
+
const signature = request.headers.get(TURN_TIMEOUT_RESUME_SIGNATURE_HEADER)?.trim() ?? "";
|
|
11098
|
+
const secret = getTurnTimeoutResumeSecret();
|
|
11099
|
+
if (!timestamp || !signature || !secret) {
|
|
11100
|
+
return void 0;
|
|
11101
|
+
}
|
|
11102
|
+
const parsedTimestamp = Number.parseInt(timestamp, 10);
|
|
11103
|
+
if (!Number.isFinite(parsedTimestamp) || Math.abs(Date.now() - parsedTimestamp) > TURN_TIMEOUT_RESUME_MAX_SKEW_MS) {
|
|
11104
|
+
return void 0;
|
|
11105
|
+
}
|
|
11106
|
+
const body = await request.text();
|
|
11107
|
+
const expectedSignature = signTurnTimeoutResumeBody(secret, timestamp, body);
|
|
11108
|
+
if (!timingSafeMatch(expectedSignature, signature)) {
|
|
11109
|
+
return void 0;
|
|
11110
|
+
}
|
|
11111
|
+
try {
|
|
11112
|
+
return parseTurnTimeoutResumeRequest(JSON.parse(body));
|
|
11113
|
+
} catch {
|
|
11114
|
+
return void 0;
|
|
10335
11115
|
}
|
|
10336
11116
|
}
|
|
10337
11117
|
|
|
@@ -10386,87 +11166,12 @@ function htmlResponse(kind) {
|
|
|
10386
11166
|
const page = CALLBACK_PAGES[kind];
|
|
10387
11167
|
return htmlCallbackResponse(page.title, page.message, page.status);
|
|
10388
11168
|
}
|
|
10389
|
-
function extractSlackText(text, files) {
|
|
10390
|
-
const message = buildSlackOutputMessage(text, files);
|
|
10391
|
-
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
10392
|
-
return message.markdown;
|
|
10393
|
-
}
|
|
10394
|
-
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
10395
|
-
return message.raw;
|
|
10396
|
-
}
|
|
10397
|
-
return text;
|
|
10398
|
-
}
|
|
10399
|
-
async function normalizeFileUploads(files) {
|
|
10400
|
-
const normalized = [];
|
|
10401
|
-
for (const file of files) {
|
|
10402
|
-
let data;
|
|
10403
|
-
if (Buffer2.isBuffer(file.data)) {
|
|
10404
|
-
data = file.data;
|
|
10405
|
-
} else if (file.data instanceof ArrayBuffer) {
|
|
10406
|
-
data = Buffer2.from(file.data);
|
|
10407
|
-
} else {
|
|
10408
|
-
data = Buffer2.from(await file.data.arrayBuffer());
|
|
10409
|
-
}
|
|
10410
|
-
normalized.push({
|
|
10411
|
-
data,
|
|
10412
|
-
filename: file.filename
|
|
10413
|
-
});
|
|
10414
|
-
}
|
|
10415
|
-
return normalized;
|
|
10416
|
-
}
|
|
10417
|
-
async function deliverReplyToThread(channelId, threadTs, reply) {
|
|
10418
|
-
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
10419
|
-
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
10420
|
-
reply,
|
|
10421
|
-
hasStreamedThreadReply: false
|
|
10422
|
-
});
|
|
10423
|
-
if (shouldPostThreadReply) {
|
|
10424
|
-
const text = extractSlackText(
|
|
10425
|
-
reply.text,
|
|
10426
|
-
attachFiles === "inline" ? replyFiles : void 0
|
|
10427
|
-
);
|
|
10428
|
-
if (text.trim().length > 0) {
|
|
10429
|
-
await postSlackMessage(channelId, threadTs, text);
|
|
10430
|
-
}
|
|
10431
|
-
}
|
|
10432
|
-
if (!replyFiles || attachFiles === "none") {
|
|
10433
|
-
return;
|
|
10434
|
-
}
|
|
10435
|
-
const files = await normalizeFileUploads(replyFiles);
|
|
10436
|
-
if (files.length === 0) {
|
|
10437
|
-
return;
|
|
10438
|
-
}
|
|
10439
|
-
try {
|
|
10440
|
-
await uploadFilesToThread({
|
|
10441
|
-
channelId,
|
|
10442
|
-
threadTs,
|
|
10443
|
-
files
|
|
10444
|
-
});
|
|
10445
|
-
} catch {
|
|
10446
|
-
}
|
|
10447
|
-
}
|
|
10448
|
-
function buildDeterministicTurnId2(messageId) {
|
|
10449
|
-
const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
10450
|
-
return `turn_${sanitized}`;
|
|
10451
|
-
}
|
|
10452
|
-
function getUserMessageIdForTurn(conversation, sessionId) {
|
|
10453
|
-
for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
|
|
10454
|
-
const message = conversation.messages[index];
|
|
10455
|
-
if (message?.role !== "user") {
|
|
10456
|
-
continue;
|
|
10457
|
-
}
|
|
10458
|
-
if (buildDeterministicTurnId2(message.id) === sessionId) {
|
|
10459
|
-
return message.id;
|
|
10460
|
-
}
|
|
10461
|
-
}
|
|
10462
|
-
return void 0;
|
|
10463
|
-
}
|
|
10464
11169
|
async function buildResumeConversationContext(channelId, threadTs, sessionId) {
|
|
10465
11170
|
const threadId = `slack:${channelId}:${threadTs}`;
|
|
10466
11171
|
const conversation = coerceThreadConversationState(
|
|
10467
11172
|
await getPersistedThreadState(threadId)
|
|
10468
11173
|
);
|
|
10469
|
-
const userMessageId =
|
|
11174
|
+
const userMessageId = getTurnUserMessageId(conversation, sessionId);
|
|
10470
11175
|
return buildConversationContext(conversation, {
|
|
10471
11176
|
excludeMessageId: userMessageId
|
|
10472
11177
|
});
|
|
@@ -10477,7 +11182,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
|
|
|
10477
11182
|
const conversation = coerceThreadConversationState(currentState);
|
|
10478
11183
|
const artifacts = coerceThreadArtifactsState(currentState);
|
|
10479
11184
|
const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
|
|
10480
|
-
const userMessageId =
|
|
11185
|
+
const userMessageId = getTurnUserMessageId(conversation, sessionId);
|
|
10481
11186
|
markConversationMessage(conversation, userMessageId, {
|
|
10482
11187
|
replied: true,
|
|
10483
11188
|
skippedReason: void 0
|
|
@@ -10514,7 +11219,7 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
|
|
|
10514
11219
|
markTurnFailed({
|
|
10515
11220
|
conversation,
|
|
10516
11221
|
nowMs: Date.now(),
|
|
10517
|
-
userMessageId:
|
|
11222
|
+
userMessageId: getTurnUserMessageId(conversation, sessionId),
|
|
10518
11223
|
markConversationMessage,
|
|
10519
11224
|
updateConversationStats
|
|
10520
11225
|
});
|
|
@@ -10527,6 +11232,17 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
10527
11232
|
if (!authSession.channelId || !authSession.threadTs) {
|
|
10528
11233
|
return;
|
|
10529
11234
|
}
|
|
11235
|
+
const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
|
|
11236
|
+
const currentState = await getPersistedThreadState(threadId);
|
|
11237
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11238
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
11239
|
+
const userMessage = getTurnUserMessage(conversation, authSession.sessionId);
|
|
11240
|
+
if (conversation.processing.activeTurnId !== authSession.sessionId) {
|
|
11241
|
+
return;
|
|
11242
|
+
}
|
|
11243
|
+
const channelConfiguration = getChannelConfigurationServiceById(
|
|
11244
|
+
authSession.channelId
|
|
11245
|
+
);
|
|
10530
11246
|
const conversationContext = await buildResumeConversationContext(
|
|
10531
11247
|
authSession.channelId,
|
|
10532
11248
|
authSession.threadTs,
|
|
@@ -10534,29 +11250,32 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
10534
11250
|
);
|
|
10535
11251
|
await resumeAuthorizedRequest({
|
|
10536
11252
|
messageText: authSession.userMessage,
|
|
10537
|
-
requesterUserId: authSession.userId,
|
|
10538
|
-
provider,
|
|
10539
11253
|
channelId: authSession.channelId,
|
|
10540
11254
|
threadTs: authSession.threadTs,
|
|
11255
|
+
lockKey: authSession.conversationId,
|
|
10541
11256
|
connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
|
|
10542
11257
|
failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
11258
|
+
replyContext: {
|
|
11259
|
+
assistant: { userName: botConfig.userName },
|
|
11260
|
+
requester: {
|
|
11261
|
+
userId: authSession.userId,
|
|
11262
|
+
userName: userMessage?.author?.userName,
|
|
11263
|
+
fullName: userMessage?.author?.fullName
|
|
11264
|
+
},
|
|
11265
|
+
correlation: {
|
|
11266
|
+
conversationId: authSession.conversationId,
|
|
11267
|
+
turnId: authSession.sessionId,
|
|
11268
|
+
channelId: authSession.channelId,
|
|
11269
|
+
threadTs: authSession.threadTs,
|
|
11270
|
+
requesterId: authSession.userId
|
|
11271
|
+
},
|
|
11272
|
+
toolChannelId: authSession.toolChannelId ?? artifacts.assistantContextChannelId ?? authSession.channelId,
|
|
11273
|
+
conversationContext,
|
|
11274
|
+
artifactState: artifacts,
|
|
11275
|
+
configuration: authSession.configuration,
|
|
11276
|
+
channelConfiguration,
|
|
11277
|
+
sandbox: getPersistedSandboxState(currentState),
|
|
11278
|
+
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
10560
11279
|
},
|
|
10561
11280
|
onSuccess: async (reply) => {
|
|
10562
11281
|
try {
|
|
@@ -10607,6 +11326,37 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
10607
11326
|
{ "app.credential.provider": provider },
|
|
10608
11327
|
"Resumed MCP turn requested another authorization flow"
|
|
10609
11328
|
);
|
|
11329
|
+
},
|
|
11330
|
+
onTimeoutPause: async (error) => {
|
|
11331
|
+
if (!isRetryableTurnError(error, "turn_timeout_resume")) {
|
|
11332
|
+
throw error;
|
|
11333
|
+
}
|
|
11334
|
+
const checkpointVersion = error.metadata?.checkpointVersion;
|
|
11335
|
+
const nextSliceId = error.metadata?.sliceId;
|
|
11336
|
+
if (typeof checkpointVersion !== "number") {
|
|
11337
|
+
throw new Error(
|
|
11338
|
+
"Timed-out MCP resume did not include a checkpoint version"
|
|
11339
|
+
);
|
|
11340
|
+
}
|
|
11341
|
+
if (!canScheduleTurnTimeoutResume(nextSliceId)) {
|
|
11342
|
+
logWarn(
|
|
11343
|
+
"mcp_oauth_callback_resume_slice_limit_reached",
|
|
11344
|
+
{},
|
|
11345
|
+
{
|
|
11346
|
+
"app.credential.provider": provider,
|
|
11347
|
+
...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
|
|
11348
|
+
},
|
|
11349
|
+
"Skipped automatic timeout resume because the turn exceeded the slice limit"
|
|
11350
|
+
);
|
|
11351
|
+
throw new Error(
|
|
11352
|
+
"Timed-out turn exceeded the automatic resume slice limit"
|
|
11353
|
+
);
|
|
11354
|
+
}
|
|
11355
|
+
await scheduleTurnTimeoutResume({
|
|
11356
|
+
conversationId: authSession.conversationId,
|
|
11357
|
+
sessionId: authSession.sessionId,
|
|
11358
|
+
expectedCheckpointVersion: checkpointVersion
|
|
11359
|
+
});
|
|
10610
11360
|
}
|
|
10611
11361
|
});
|
|
10612
11362
|
}
|
|
@@ -10823,14 +11573,15 @@ async function resumePendingOAuthMessage(stored) {
|
|
|
10823
11573
|
);
|
|
10824
11574
|
await resumeAuthorizedRequest({
|
|
10825
11575
|
messageText: stored.pendingMessage,
|
|
10826
|
-
requesterUserId: stored.userId,
|
|
10827
|
-
provider: stored.provider,
|
|
10828
11576
|
channelId: stored.channelId,
|
|
10829
11577
|
threadTs: stored.threadTs,
|
|
10830
11578
|
connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
|
|
10831
11579
|
failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
|
|
10832
|
-
|
|
10833
|
-
|
|
11580
|
+
replyContext: {
|
|
11581
|
+
requester: { userId: stored.userId },
|
|
11582
|
+
conversationContext,
|
|
11583
|
+
configuration: stored.configuration
|
|
11584
|
+
},
|
|
10834
11585
|
onSuccess: async (reply) => {
|
|
10835
11586
|
logInfo(
|
|
10836
11587
|
"oauth_callback_resume_complete",
|
|
@@ -10990,38 +11741,304 @@ async function GET5(request, provider, waitUntil) {
|
|
|
10990
11741
|
} catch {
|
|
10991
11742
|
}
|
|
10992
11743
|
});
|
|
10993
|
-
if (stored.pendingMessage && stored.channelId && stored.threadTs) {
|
|
10994
|
-
waitUntil(() => resumePendingOAuthMessage(stored));
|
|
10995
|
-
} else if (stored.channelId && stored.threadTs) {
|
|
10996
|
-
const { channelId, threadTs } = stored;
|
|
10997
|
-
waitUntil(
|
|
10998
|
-
() => postSlackMessage(
|
|
10999
|
-
channelId,
|
|
11000
|
-
threadTs,
|
|
11001
|
-
`Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
11002
|
-
)
|
|
11003
|
-
);
|
|
11744
|
+
if (stored.pendingMessage && stored.channelId && stored.threadTs) {
|
|
11745
|
+
waitUntil(() => resumePendingOAuthMessage(stored));
|
|
11746
|
+
} else if (stored.channelId && stored.threadTs) {
|
|
11747
|
+
const { channelId, threadTs } = stored;
|
|
11748
|
+
waitUntil(
|
|
11749
|
+
() => postSlackMessage(
|
|
11750
|
+
channelId,
|
|
11751
|
+
threadTs,
|
|
11752
|
+
`Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
11753
|
+
)
|
|
11754
|
+
);
|
|
11755
|
+
}
|
|
11756
|
+
const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
|
|
11757
|
+
const html = `<!DOCTYPE html>
|
|
11758
|
+
<html>
|
|
11759
|
+
<head><title>${providerLabel} Connected</title></head>
|
|
11760
|
+
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
11761
|
+
<div style="text-align: center;">
|
|
11762
|
+
<h1>${providerLabel} account connected</h1>
|
|
11763
|
+
<p>${statusMessage}</p>
|
|
11764
|
+
</div>
|
|
11765
|
+
</body>
|
|
11766
|
+
</html>`;
|
|
11767
|
+
return new Response(html, {
|
|
11768
|
+
status: 200,
|
|
11769
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
11770
|
+
});
|
|
11771
|
+
}
|
|
11772
|
+
|
|
11773
|
+
// src/chat/slack/context.ts
|
|
11774
|
+
function toTrimmedSlackString(value) {
|
|
11775
|
+
const normalized = toOptionalString(value);
|
|
11776
|
+
return normalized?.trim() || void 0;
|
|
11777
|
+
}
|
|
11778
|
+
function parseSlackThreadId(threadId) {
|
|
11779
|
+
const normalizedThreadId = toTrimmedSlackString(threadId);
|
|
11780
|
+
if (!normalizedThreadId) {
|
|
11781
|
+
return void 0;
|
|
11782
|
+
}
|
|
11783
|
+
const parts = normalizedThreadId.split(":");
|
|
11784
|
+
if (parts.length !== 3 || parts[0] !== "slack") {
|
|
11785
|
+
return void 0;
|
|
11786
|
+
}
|
|
11787
|
+
const channelId = toTrimmedSlackString(parts[1]);
|
|
11788
|
+
const threadTs = toTrimmedSlackString(parts[2]);
|
|
11789
|
+
if (!channelId || !threadTs) {
|
|
11790
|
+
return void 0;
|
|
11791
|
+
}
|
|
11792
|
+
return { channelId, threadTs };
|
|
11793
|
+
}
|
|
11794
|
+
function resolveSlackChannelIdFromThreadId(threadId) {
|
|
11795
|
+
return parseSlackThreadId(threadId)?.channelId;
|
|
11796
|
+
}
|
|
11797
|
+
function resolveSlackChannelIdFromMessage(message) {
|
|
11798
|
+
const messageChannelId = toTrimmedSlackString(
|
|
11799
|
+
message.channelId
|
|
11800
|
+
);
|
|
11801
|
+
if (messageChannelId) {
|
|
11802
|
+
return messageChannelId;
|
|
11803
|
+
}
|
|
11804
|
+
const raw = message.raw;
|
|
11805
|
+
if (raw && typeof raw === "object") {
|
|
11806
|
+
const rawChannel = toTrimmedSlackString(
|
|
11807
|
+
raw.channel
|
|
11808
|
+
);
|
|
11809
|
+
if (rawChannel) {
|
|
11810
|
+
return rawChannel;
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
const threadId = toTrimmedSlackString(
|
|
11814
|
+
message.threadId
|
|
11815
|
+
);
|
|
11816
|
+
return resolveSlackChannelIdFromThreadId(threadId);
|
|
11817
|
+
}
|
|
11818
|
+
|
|
11819
|
+
// src/handlers/turn-resume.ts
|
|
11820
|
+
async function persistCompletedReplyState2(args) {
|
|
11821
|
+
const currentState = await getPersistedThreadState(
|
|
11822
|
+
args.checkpoint.conversationId
|
|
11823
|
+
);
|
|
11824
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11825
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
11826
|
+
const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
|
|
11827
|
+
const userMessage = getTurnUserMessage(
|
|
11828
|
+
conversation,
|
|
11829
|
+
args.checkpoint.sessionId
|
|
11830
|
+
);
|
|
11831
|
+
markConversationMessage(conversation, userMessage?.id, {
|
|
11832
|
+
replied: true,
|
|
11833
|
+
skippedReason: void 0
|
|
11834
|
+
});
|
|
11835
|
+
upsertConversationMessage(conversation, {
|
|
11836
|
+
id: generateConversationId("assistant"),
|
|
11837
|
+
role: "assistant",
|
|
11838
|
+
text: normalizeConversationText(args.reply.text) || "[empty response]",
|
|
11839
|
+
createdAtMs: Date.now(),
|
|
11840
|
+
author: {
|
|
11841
|
+
userName: botConfig.userName,
|
|
11842
|
+
isBot: true
|
|
11843
|
+
},
|
|
11844
|
+
meta: {
|
|
11845
|
+
replied: true
|
|
11846
|
+
}
|
|
11847
|
+
});
|
|
11848
|
+
markTurnCompleted({
|
|
11849
|
+
conversation,
|
|
11850
|
+
nowMs: Date.now(),
|
|
11851
|
+
updateConversationStats
|
|
11852
|
+
});
|
|
11853
|
+
await persistThreadStateById(args.checkpoint.conversationId, {
|
|
11854
|
+
artifacts: nextArtifacts,
|
|
11855
|
+
conversation,
|
|
11856
|
+
sandboxId: args.reply.sandboxId,
|
|
11857
|
+
sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
|
|
11858
|
+
});
|
|
11859
|
+
}
|
|
11860
|
+
async function persistFailedReplyState2(checkpoint) {
|
|
11861
|
+
const currentState = await getPersistedThreadState(checkpoint.conversationId);
|
|
11862
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11863
|
+
markTurnFailed({
|
|
11864
|
+
conversation,
|
|
11865
|
+
nowMs: Date.now(),
|
|
11866
|
+
userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
|
|
11867
|
+
markConversationMessage,
|
|
11868
|
+
updateConversationStats
|
|
11869
|
+
});
|
|
11870
|
+
await persistThreadStateById(checkpoint.conversationId, {
|
|
11871
|
+
conversation
|
|
11872
|
+
});
|
|
11873
|
+
}
|
|
11874
|
+
async function resumeTimedOutTurn(payload) {
|
|
11875
|
+
const checkpoint = await getAgentTurnSessionCheckpoint(
|
|
11876
|
+
payload.conversationId,
|
|
11877
|
+
payload.sessionId
|
|
11878
|
+
);
|
|
11879
|
+
if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
|
|
11880
|
+
return;
|
|
11881
|
+
}
|
|
11882
|
+
const thread = parseSlackThreadId(payload.conversationId);
|
|
11883
|
+
if (!thread) {
|
|
11884
|
+
throw new Error(
|
|
11885
|
+
`Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
|
|
11886
|
+
);
|
|
11887
|
+
}
|
|
11888
|
+
const currentState = await getPersistedThreadState(payload.conversationId);
|
|
11889
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
11890
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
11891
|
+
const userMessage = getTurnUserMessage(conversation, payload.sessionId);
|
|
11892
|
+
if (!userMessage?.author?.userId) {
|
|
11893
|
+
throw new Error(
|
|
11894
|
+
`Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
|
|
11895
|
+
);
|
|
11896
|
+
}
|
|
11897
|
+
if (conversation.processing.activeTurnId !== payload.sessionId) {
|
|
11898
|
+
return;
|
|
11899
|
+
}
|
|
11900
|
+
const channelConfiguration = getChannelConfigurationServiceById(
|
|
11901
|
+
thread.channelId
|
|
11902
|
+
);
|
|
11903
|
+
const conversationContext = buildConversationContext(conversation, {
|
|
11904
|
+
excludeMessageId: userMessage.id
|
|
11905
|
+
});
|
|
11906
|
+
const sandbox = getPersistedSandboxState(currentState);
|
|
11907
|
+
await resumeSlackTurn({
|
|
11908
|
+
messageText: userMessage.text,
|
|
11909
|
+
channelId: thread.channelId,
|
|
11910
|
+
threadTs: thread.threadTs,
|
|
11911
|
+
lockKey: payload.conversationId,
|
|
11912
|
+
failureText: "I hit an error while resuming that request. Please try the command again.",
|
|
11913
|
+
replyContext: {
|
|
11914
|
+
assistant: { userName: botConfig.userName },
|
|
11915
|
+
requester: {
|
|
11916
|
+
userId: userMessage.author.userId,
|
|
11917
|
+
userName: userMessage.author.userName,
|
|
11918
|
+
fullName: userMessage.author.fullName
|
|
11919
|
+
},
|
|
11920
|
+
correlation: {
|
|
11921
|
+
conversationId: payload.conversationId,
|
|
11922
|
+
turnId: payload.sessionId,
|
|
11923
|
+
channelId: thread.channelId,
|
|
11924
|
+
threadTs: thread.threadTs,
|
|
11925
|
+
requesterId: userMessage.author.userId
|
|
11926
|
+
},
|
|
11927
|
+
toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
|
|
11928
|
+
artifactState: artifacts,
|
|
11929
|
+
conversationContext,
|
|
11930
|
+
channelConfiguration,
|
|
11931
|
+
sandbox,
|
|
11932
|
+
threadParticipants: buildThreadParticipants(conversation.messages)
|
|
11933
|
+
},
|
|
11934
|
+
onSuccess: async (reply) => {
|
|
11935
|
+
try {
|
|
11936
|
+
await persistCompletedReplyState2({ checkpoint, reply });
|
|
11937
|
+
} catch (persistError) {
|
|
11938
|
+
logException(
|
|
11939
|
+
persistError,
|
|
11940
|
+
"timeout_resume_complete_persist_failed",
|
|
11941
|
+
{},
|
|
11942
|
+
{
|
|
11943
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
11944
|
+
"app.ai.session_id": payload.sessionId
|
|
11945
|
+
},
|
|
11946
|
+
"Failed to persist completed timeout-resume state after reply delivery"
|
|
11947
|
+
);
|
|
11948
|
+
}
|
|
11949
|
+
},
|
|
11950
|
+
onFailure: async (error) => {
|
|
11951
|
+
logException(
|
|
11952
|
+
error,
|
|
11953
|
+
"timeout_resume_failed",
|
|
11954
|
+
{},
|
|
11955
|
+
{
|
|
11956
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
11957
|
+
"app.ai.session_id": payload.sessionId
|
|
11958
|
+
},
|
|
11959
|
+
"Failed to resume timed-out turn"
|
|
11960
|
+
);
|
|
11961
|
+
await persistFailedReplyState2(checkpoint);
|
|
11962
|
+
},
|
|
11963
|
+
onAuthPause: async () => {
|
|
11964
|
+
logWarn(
|
|
11965
|
+
"timeout_resume_reparked_for_auth",
|
|
11966
|
+
{},
|
|
11967
|
+
{
|
|
11968
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
11969
|
+
"app.ai.session_id": payload.sessionId
|
|
11970
|
+
},
|
|
11971
|
+
"Resumed timed-out turn parked for auth"
|
|
11972
|
+
);
|
|
11973
|
+
},
|
|
11974
|
+
onTimeoutPause: async (error) => {
|
|
11975
|
+
if (!isRetryableTurnError(error, "turn_timeout_resume")) {
|
|
11976
|
+
throw error;
|
|
11977
|
+
}
|
|
11978
|
+
const checkpointVersion = error.metadata?.checkpointVersion;
|
|
11979
|
+
const nextSliceId = error.metadata?.sliceId;
|
|
11980
|
+
if (typeof checkpointVersion !== "number") {
|
|
11981
|
+
throw new Error(
|
|
11982
|
+
"Timed-out resume turn did not include a checkpoint version"
|
|
11983
|
+
);
|
|
11984
|
+
}
|
|
11985
|
+
if (!canScheduleTurnTimeoutResume(nextSliceId)) {
|
|
11986
|
+
logWarn(
|
|
11987
|
+
"timeout_resume_slice_limit_reached",
|
|
11988
|
+
{},
|
|
11989
|
+
{
|
|
11990
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
11991
|
+
"app.ai.session_id": payload.sessionId,
|
|
11992
|
+
...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
|
|
11993
|
+
},
|
|
11994
|
+
"Skipped automatic timeout resume because the turn exceeded the slice limit"
|
|
11995
|
+
);
|
|
11996
|
+
throw new Error(
|
|
11997
|
+
"Timed-out turn exceeded the automatic resume slice limit"
|
|
11998
|
+
);
|
|
11999
|
+
}
|
|
12000
|
+
await scheduleTurnTimeoutResume({
|
|
12001
|
+
conversationId: payload.conversationId,
|
|
12002
|
+
sessionId: payload.sessionId,
|
|
12003
|
+
expectedCheckpointVersion: checkpointVersion
|
|
12004
|
+
});
|
|
12005
|
+
}
|
|
12006
|
+
});
|
|
12007
|
+
}
|
|
12008
|
+
async function POST(request, waitUntil) {
|
|
12009
|
+
const payload = await verifyTurnTimeoutResumeRequest(request);
|
|
12010
|
+
if (!payload) {
|
|
12011
|
+
return new Response("Unauthorized", { status: 401 });
|
|
11004
12012
|
}
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
12013
|
+
waitUntil(
|
|
12014
|
+
() => resumeTimedOutTurn(payload).catch((error) => {
|
|
12015
|
+
if (error instanceof ResumeTurnBusyError) {
|
|
12016
|
+
logWarn(
|
|
12017
|
+
"timeout_resume_lock_busy",
|
|
12018
|
+
{},
|
|
12019
|
+
{
|
|
12020
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
12021
|
+
"app.ai.session_id": payload.sessionId
|
|
12022
|
+
},
|
|
12023
|
+
"Skipped timeout resume because another turn owns the thread lock"
|
|
12024
|
+
);
|
|
12025
|
+
return;
|
|
12026
|
+
}
|
|
12027
|
+
logException(
|
|
12028
|
+
error,
|
|
12029
|
+
"timeout_resume_handler_failed",
|
|
12030
|
+
{},
|
|
12031
|
+
{
|
|
12032
|
+
"app.ai.conversation_id": payload.conversationId,
|
|
12033
|
+
"app.ai.session_id": payload.sessionId
|
|
12034
|
+
},
|
|
12035
|
+
"Timeout resume handler failed"
|
|
12036
|
+
);
|
|
12037
|
+
})
|
|
12038
|
+
);
|
|
12039
|
+
return new Response("Accepted", { status: 202 });
|
|
11020
12040
|
}
|
|
11021
12041
|
|
|
11022
|
-
// src/chat/app/production.ts
|
|
11023
|
-
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
11024
|
-
|
|
11025
12042
|
// src/chat/services/subscribed-decision.ts
|
|
11026
12043
|
import { z } from "zod";
|
|
11027
12044
|
var replyDecisionSchema = z.object({
|
|
@@ -11048,11 +12065,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
|
|
|
11048
12065
|
var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
|
|
11049
12066
|
var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
|
|
11050
12067
|
var RECENT_THREAD_WINDOW = 6;
|
|
11051
|
-
function
|
|
12068
|
+
function escapeRegExp2(value) {
|
|
11052
12069
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11053
12070
|
}
|
|
11054
12071
|
function containsAssistantInvocation(text, botUserName) {
|
|
11055
|
-
const escapedUserName =
|
|
12072
|
+
const escapedUserName = escapeRegExp2(botUserName);
|
|
11056
12073
|
const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
|
|
11057
12074
|
const labeledEntityMentionRe = new RegExp(
|
|
11058
12075
|
`<@[^>|]+\\|${escapedUserName}>`,
|
|
@@ -11340,54 +12357,8 @@ async function decideSubscribedThreadReply(args) {
|
|
|
11340
12357
|
}
|
|
11341
12358
|
}
|
|
11342
12359
|
|
|
11343
|
-
// src/chat/slack/context.ts
|
|
11344
|
-
function toTrimmedSlackString(value) {
|
|
11345
|
-
const normalized = toOptionalString(value);
|
|
11346
|
-
return normalized?.trim() || void 0;
|
|
11347
|
-
}
|
|
11348
|
-
function parseSlackThreadId(threadId) {
|
|
11349
|
-
const normalizedThreadId = toTrimmedSlackString(threadId);
|
|
11350
|
-
if (!normalizedThreadId) {
|
|
11351
|
-
return void 0;
|
|
11352
|
-
}
|
|
11353
|
-
const parts = normalizedThreadId.split(":");
|
|
11354
|
-
if (parts.length !== 3 || parts[0] !== "slack") {
|
|
11355
|
-
return void 0;
|
|
11356
|
-
}
|
|
11357
|
-
const channelId = toTrimmedSlackString(parts[1]);
|
|
11358
|
-
const threadTs = toTrimmedSlackString(parts[2]);
|
|
11359
|
-
if (!channelId || !threadTs) {
|
|
11360
|
-
return void 0;
|
|
11361
|
-
}
|
|
11362
|
-
return { channelId, threadTs };
|
|
11363
|
-
}
|
|
11364
|
-
function resolveSlackChannelIdFromThreadId(threadId) {
|
|
11365
|
-
return parseSlackThreadId(threadId)?.channelId;
|
|
11366
|
-
}
|
|
11367
|
-
function resolveSlackChannelIdFromMessage(message) {
|
|
11368
|
-
const messageChannelId = toTrimmedSlackString(
|
|
11369
|
-
message.channelId
|
|
11370
|
-
);
|
|
11371
|
-
if (messageChannelId) {
|
|
11372
|
-
return messageChannelId;
|
|
11373
|
-
}
|
|
11374
|
-
const raw = message.raw;
|
|
11375
|
-
if (raw && typeof raw === "object") {
|
|
11376
|
-
const rawChannel = toTrimmedSlackString(
|
|
11377
|
-
raw.channel
|
|
11378
|
-
);
|
|
11379
|
-
if (rawChannel) {
|
|
11380
|
-
return rawChannel;
|
|
11381
|
-
}
|
|
11382
|
-
}
|
|
11383
|
-
const threadId = toTrimmedSlackString(
|
|
11384
|
-
message.threadId
|
|
11385
|
-
);
|
|
11386
|
-
return resolveSlackChannelIdFromThreadId(threadId);
|
|
11387
|
-
}
|
|
11388
|
-
|
|
11389
12360
|
// src/chat/runtime/thread-context.ts
|
|
11390
|
-
function
|
|
12361
|
+
function escapeRegExp3(value) {
|
|
11391
12362
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11392
12363
|
}
|
|
11393
12364
|
function stripLeadingBotMention(text, options = {}) {
|
|
@@ -11397,12 +12368,12 @@ function stripLeadingBotMention(text, options = {}) {
|
|
|
11397
12368
|
next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
|
|
11398
12369
|
}
|
|
11399
12370
|
const mentionByNameRe = new RegExp(
|
|
11400
|
-
`^\\s*@${
|
|
12371
|
+
`^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
|
|
11401
12372
|
"i"
|
|
11402
12373
|
);
|
|
11403
12374
|
next = next.replace(mentionByNameRe, "").trim();
|
|
11404
12375
|
const mentionByLabeledEntityRe = new RegExp(
|
|
11405
|
-
`^\\s*<@[^>|]+\\|${
|
|
12376
|
+
`^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
|
|
11406
12377
|
"i"
|
|
11407
12378
|
);
|
|
11408
12379
|
next = next.replace(mentionByLabeledEntityRe, "").trim();
|
|
@@ -11914,6 +12885,15 @@ var MAX_USER_ATTACHMENTS = 3;
|
|
|
11914
12885
|
var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
11915
12886
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
11916
12887
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
12888
|
+
function hasPotentialImageAttachment(attachments) {
|
|
12889
|
+
return attachments?.some((attachment) => {
|
|
12890
|
+
if (attachment.type === "image") {
|
|
12891
|
+
return true;
|
|
12892
|
+
}
|
|
12893
|
+
const mimeType = attachment.mimeType ?? "";
|
|
12894
|
+
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
12895
|
+
}) ?? false;
|
|
12896
|
+
}
|
|
11917
12897
|
function isVisionEnabled() {
|
|
11918
12898
|
return Boolean(botConfig.visionModelId);
|
|
11919
12899
|
}
|
|
@@ -12256,6 +13236,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
12256
13236
|
continue;
|
|
12257
13237
|
}
|
|
12258
13238
|
hydratedMessageIds.add(conversationMessage.id);
|
|
13239
|
+
const existingMeta = conversationMessage.meta ?? {};
|
|
12259
13240
|
const imageFiles = (reply.files ?? []).filter((file) => {
|
|
12260
13241
|
const mimeType = toOptionalString(file.mimetype);
|
|
12261
13242
|
return Boolean(
|
|
@@ -12263,10 +13244,15 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
12263
13244
|
);
|
|
12264
13245
|
}).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
|
|
12265
13246
|
if (imageFiles.length === 0) {
|
|
13247
|
+
conversationMessage.meta = {
|
|
13248
|
+
...existingMeta,
|
|
13249
|
+
slackTs: existingMeta.slackTs ?? ts,
|
|
13250
|
+
imagesHydrated: true
|
|
13251
|
+
};
|
|
13252
|
+
mutated = true;
|
|
12266
13253
|
continue;
|
|
12267
13254
|
}
|
|
12268
13255
|
const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
|
|
12269
|
-
const existingMeta = conversationMessage.meta ?? {};
|
|
12270
13256
|
conversationMessage.meta = {
|
|
12271
13257
|
...existingMeta,
|
|
12272
13258
|
slackTs: existingMeta.slackTs ?? ts,
|
|
@@ -12430,6 +13416,7 @@ function createJuniorRuntimeServices(overrides = {}) {
|
|
|
12430
13416
|
replyExecutor: {
|
|
12431
13417
|
generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
|
|
12432
13418
|
lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
|
|
13419
|
+
scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
|
|
12433
13420
|
generateThreadTitle: conversationMemory.generateThreadTitle
|
|
12434
13421
|
},
|
|
12435
13422
|
subscribedReplyPolicy: createSubscribedReplyPolicy({
|
|
@@ -12494,18 +13481,13 @@ function getExecutionFailureReason(reply) {
|
|
|
12494
13481
|
}
|
|
12495
13482
|
return "empty assistant turn";
|
|
12496
13483
|
}
|
|
12497
|
-
function
|
|
12498
|
-
const
|
|
12499
|
-
const
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
if (!userId || message.author?.isBot) continue;
|
|
12503
|
-
if (!seen.has(userId)) {
|
|
12504
|
-
seen.add(userId);
|
|
12505
|
-
participants.push({ userId, userName, fullName });
|
|
12506
|
-
}
|
|
13484
|
+
function shouldAutoStartStreaming(args) {
|
|
13485
|
+
const { text, deltaCount } = args;
|
|
13486
|
+
const trimmed = text.trim();
|
|
13487
|
+
if (!trimmed || isPotentialRedundantReactionAckText(trimmed)) {
|
|
13488
|
+
return false;
|
|
12507
13489
|
}
|
|
12508
|
-
return
|
|
13490
|
+
return deltaCount >= 2;
|
|
12509
13491
|
}
|
|
12510
13492
|
function createReplyToThread(deps) {
|
|
12511
13493
|
return async function replyToThread(thread, message, options = {}) {
|
|
@@ -12615,7 +13597,10 @@ function createReplyToThread(deps) {
|
|
|
12615
13597
|
const textStream = createTextStreamBridge();
|
|
12616
13598
|
let streamedReplyPromise;
|
|
12617
13599
|
let pendingStreamText = "";
|
|
13600
|
+
let pendingStreamDeltaCount = 0;
|
|
13601
|
+
let awaitingPostToolAssistantMessage = false;
|
|
12618
13602
|
let beforeFirstResponsePostCalled = false;
|
|
13603
|
+
let streamedReplyState = createSlackStreamAccumulator();
|
|
12619
13604
|
const beforeFirstResponsePost = async () => {
|
|
12620
13605
|
if (beforeFirstResponsePostCalled) {
|
|
12621
13606
|
return;
|
|
@@ -12641,6 +13626,51 @@ function createReplyToThread(deps) {
|
|
|
12641
13626
|
startStreamingReply();
|
|
12642
13627
|
textStream.push(pendingStreamText);
|
|
12643
13628
|
pendingStreamText = "";
|
|
13629
|
+
pendingStreamDeltaCount = 0;
|
|
13630
|
+
};
|
|
13631
|
+
const clearPendingStreamText = () => {
|
|
13632
|
+
pendingStreamText = "";
|
|
13633
|
+
pendingStreamDeltaCount = 0;
|
|
13634
|
+
};
|
|
13635
|
+
const discardPendingStreamPreview = () => {
|
|
13636
|
+
clearPendingStreamText();
|
|
13637
|
+
streamedReplyState = createSlackStreamAccumulator();
|
|
13638
|
+
};
|
|
13639
|
+
const finalizePendingStreamText = () => {
|
|
13640
|
+
if (!pendingStreamText || streamedReplyPromise || isPotentialRedundantReactionAckText(pendingStreamText)) {
|
|
13641
|
+
return;
|
|
13642
|
+
}
|
|
13643
|
+
flushPendingStreamText();
|
|
13644
|
+
};
|
|
13645
|
+
const queueVisibleStreamText = (text) => {
|
|
13646
|
+
if (!text) {
|
|
13647
|
+
return;
|
|
13648
|
+
}
|
|
13649
|
+
if (awaitingPostToolAssistantMessage) {
|
|
13650
|
+
return;
|
|
13651
|
+
}
|
|
13652
|
+
if (streamedReplyPromise) {
|
|
13653
|
+
textStream.push(text);
|
|
13654
|
+
return;
|
|
13655
|
+
}
|
|
13656
|
+
pendingStreamText += text;
|
|
13657
|
+
pendingStreamDeltaCount += 1;
|
|
13658
|
+
if (isPotentialRedundantReactionAckText(pendingStreamText)) {
|
|
13659
|
+
return;
|
|
13660
|
+
}
|
|
13661
|
+
if (!shouldAutoStartStreaming({
|
|
13662
|
+
text: pendingStreamText,
|
|
13663
|
+
deltaCount: pendingStreamDeltaCount
|
|
13664
|
+
})) {
|
|
13665
|
+
return;
|
|
13666
|
+
}
|
|
13667
|
+
flushPendingStreamText();
|
|
13668
|
+
};
|
|
13669
|
+
const appendVisibleStreamDelta = (deltaText) => {
|
|
13670
|
+
if (awaitingPostToolAssistantMessage && !streamedReplyPromise) {
|
|
13671
|
+
return;
|
|
13672
|
+
}
|
|
13673
|
+
queueVisibleStreamText(streamedReplyState.append(deltaText));
|
|
12644
13674
|
};
|
|
12645
13675
|
const postThreadReply = async (payload, stage) => {
|
|
12646
13676
|
await beforeFirstResponsePost();
|
|
@@ -12666,7 +13696,7 @@ function createReplyToThread(deps) {
|
|
|
12666
13696
|
let shouldPersistFailureState = true;
|
|
12667
13697
|
try {
|
|
12668
13698
|
const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
|
|
12669
|
-
const threadParticipants =
|
|
13699
|
+
const threadParticipants = buildThreadParticipants(
|
|
12670
13700
|
preparedState.conversation.messages
|
|
12671
13701
|
);
|
|
12672
13702
|
const reply = await deps.services.generateAssistantReply(userText, {
|
|
@@ -12698,25 +13728,41 @@ function createReplyToThread(deps) {
|
|
|
12698
13728
|
sandboxId: preparedState.sandboxId,
|
|
12699
13729
|
sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
|
|
12700
13730
|
},
|
|
13731
|
+
onSandboxAcquired: async (sandbox) => {
|
|
13732
|
+
await persistThreadState(thread, {
|
|
13733
|
+
sandboxId: sandbox.sandboxId,
|
|
13734
|
+
sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
|
|
13735
|
+
});
|
|
13736
|
+
},
|
|
13737
|
+
onArtifactStateUpdated: async (artifacts) => {
|
|
13738
|
+
await persistThreadState(thread, { artifacts });
|
|
13739
|
+
},
|
|
12701
13740
|
threadParticipants,
|
|
12702
13741
|
onStatus: (status) => progress.setStatus(status),
|
|
12703
13742
|
onTextDelta: (deltaText) => {
|
|
12704
13743
|
if (explicitChannelPostIntent) {
|
|
12705
13744
|
return;
|
|
12706
13745
|
}
|
|
12707
|
-
|
|
12708
|
-
|
|
13746
|
+
appendVisibleStreamDelta(deltaText);
|
|
13747
|
+
},
|
|
13748
|
+
onAssistantMessageStart: () => {
|
|
13749
|
+
if (!awaitingPostToolAssistantMessage) {
|
|
12709
13750
|
return;
|
|
12710
13751
|
}
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
13752
|
+
awaitingPostToolAssistantMessage = false;
|
|
13753
|
+
discardPendingStreamPreview();
|
|
13754
|
+
},
|
|
13755
|
+
onToolCall: () => {
|
|
13756
|
+
if (!streamedReplyPromise) {
|
|
13757
|
+
awaitingPostToolAssistantMessage = true;
|
|
13758
|
+
discardPendingStreamPreview();
|
|
12714
13759
|
}
|
|
12715
|
-
flushPendingStreamText();
|
|
12716
13760
|
}
|
|
12717
13761
|
});
|
|
12718
13762
|
if (streamedReplyPromise) {
|
|
12719
13763
|
flushPendingStreamText();
|
|
13764
|
+
} else {
|
|
13765
|
+
finalizePendingStreamText();
|
|
12720
13766
|
}
|
|
12721
13767
|
textStream.end();
|
|
12722
13768
|
const diagnosticsContext = {
|
|
@@ -12790,28 +13836,32 @@ function createReplyToThread(deps) {
|
|
|
12790
13836
|
}
|
|
12791
13837
|
});
|
|
12792
13838
|
const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
|
|
12793
|
-
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
12794
|
-
const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
|
|
12795
|
-
reply,
|
|
12796
|
-
hasStreamedThreadReply: Boolean(streamedReplyPromise)
|
|
12797
|
-
});
|
|
12798
13839
|
const reactionPerformed = reply.diagnostics.toolCalls.includes(
|
|
12799
13840
|
"slackMessageAddReaction"
|
|
12800
13841
|
);
|
|
12801
|
-
|
|
13842
|
+
const plannedPosts = planSlackReplyPosts({
|
|
13843
|
+
reply,
|
|
13844
|
+
hasStreamedThreadReply: Boolean(streamedReplyPromise),
|
|
13845
|
+
streamedOverflowText: streamedReplyState.getOverflowText()
|
|
13846
|
+
});
|
|
13847
|
+
if (streamedReplyPromise) {
|
|
13848
|
+
await streamedReplyPromise;
|
|
13849
|
+
}
|
|
13850
|
+
if (plannedPosts.length > 0) {
|
|
12802
13851
|
if (!streamedReplyPromise) {
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
)
|
|
12810
|
-
if (reactionPerformed && isRedundantReactionAckText(reply.text)) {
|
|
13852
|
+
let sent;
|
|
13853
|
+
for (const post of plannedPosts) {
|
|
13854
|
+
sent = await postThreadReply(post.message, post.stage);
|
|
13855
|
+
}
|
|
13856
|
+
const firstPlannedMessage = plannedPosts[0]?.message;
|
|
13857
|
+
const firstPlannedMessageHasFiles = typeof firstPlannedMessage === "object" && firstPlannedMessage !== null && "files" in firstPlannedMessage && Array.isArray(firstPlannedMessage.files) && firstPlannedMessage.files.length > 0;
|
|
13858
|
+
if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
|
|
12811
13859
|
await sent.delete();
|
|
12812
13860
|
}
|
|
12813
13861
|
} else {
|
|
12814
|
-
|
|
13862
|
+
for (const post of plannedPosts) {
|
|
13863
|
+
await postThreadReply(post.message, post.stage);
|
|
13864
|
+
}
|
|
12815
13865
|
}
|
|
12816
13866
|
}
|
|
12817
13867
|
const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
|
|
@@ -12887,17 +13937,67 @@ function createReplyToThread(deps) {
|
|
|
12887
13937
|
);
|
|
12888
13938
|
});
|
|
12889
13939
|
}
|
|
12890
|
-
if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
|
|
12891
|
-
await postThreadReply(
|
|
12892
|
-
buildSlackOutputMessage("", replyFiles),
|
|
12893
|
-
"thread_reply_files_followup"
|
|
12894
|
-
);
|
|
12895
|
-
}
|
|
12896
13940
|
} catch (error) {
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
13941
|
+
if (isRetryableTurnError(error, "mcp_auth_resume")) {
|
|
13942
|
+
shouldPersistFailureState = false;
|
|
13943
|
+
throw error;
|
|
13944
|
+
}
|
|
13945
|
+
if (isRetryableTurnError(error, "turn_timeout_resume")) {
|
|
13946
|
+
textStream.end();
|
|
13947
|
+
const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
|
|
13948
|
+
if (hasVisibleAssistantOutput) {
|
|
13949
|
+
logWarn(
|
|
13950
|
+
"agent_turn_timeout_resume_skipped_after_visible_output",
|
|
13951
|
+
turnTraceContext,
|
|
13952
|
+
messageTs ? { "messaging.message.id": messageTs } : {},
|
|
13953
|
+
"Skipped automatic timeout resume because assistant text had already started streaming"
|
|
13954
|
+
);
|
|
13955
|
+
}
|
|
13956
|
+
const conversationIdForResume = error.metadata?.conversationId;
|
|
13957
|
+
const sessionIdForResume = error.metadata?.sessionId;
|
|
13958
|
+
const checkpointVersion = error.metadata?.checkpointVersion;
|
|
13959
|
+
const nextSliceId = error.metadata?.sliceId;
|
|
13960
|
+
if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
|
|
13961
|
+
try {
|
|
13962
|
+
await deps.services.scheduleTurnTimeoutResume({
|
|
13963
|
+
conversationId: conversationIdForResume,
|
|
13964
|
+
sessionId: sessionIdForResume,
|
|
13965
|
+
expectedCheckpointVersion: checkpointVersion
|
|
13966
|
+
});
|
|
13967
|
+
shouldPersistFailureState = false;
|
|
13968
|
+
return;
|
|
13969
|
+
} catch (scheduleError) {
|
|
13970
|
+
logException(
|
|
13971
|
+
scheduleError,
|
|
13972
|
+
"agent_turn_timeout_resume_schedule_failed",
|
|
13973
|
+
turnTraceContext,
|
|
13974
|
+
{
|
|
13975
|
+
...messageTs ? { "messaging.message.id": messageTs } : {},
|
|
13976
|
+
"app.ai.resume_checkpoint_version": checkpointVersion
|
|
13977
|
+
},
|
|
13978
|
+
"Failed to schedule timeout resume callback"
|
|
13979
|
+
);
|
|
13980
|
+
}
|
|
13981
|
+
} else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
|
|
13982
|
+
logWarn(
|
|
13983
|
+
"agent_turn_timeout_resume_slice_limit_reached",
|
|
13984
|
+
turnTraceContext,
|
|
13985
|
+
{
|
|
13986
|
+
...messageTs ? { "messaging.message.id": messageTs } : {},
|
|
13987
|
+
...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
|
|
13988
|
+
},
|
|
13989
|
+
"Skipped automatic timeout resume because the turn exceeded the slice limit"
|
|
13990
|
+
);
|
|
13991
|
+
} else {
|
|
13992
|
+
logWarn(
|
|
13993
|
+
"agent_turn_timeout_resume_metadata_missing",
|
|
13994
|
+
turnTraceContext,
|
|
13995
|
+
messageTs ? { "messaging.message.id": messageTs } : {},
|
|
13996
|
+
"Timed-out turn could not be scheduled for resume because retry metadata was incomplete"
|
|
13997
|
+
);
|
|
13998
|
+
}
|
|
13999
|
+
}
|
|
14000
|
+
shouldPersistFailureState = true;
|
|
12901
14001
|
throw error;
|
|
12902
14002
|
} finally {
|
|
12903
14003
|
textStream.end();
|
|
@@ -12968,6 +14068,11 @@ async function initializeAssistantThread(event) {
|
|
|
12968
14068
|
}
|
|
12969
14069
|
|
|
12970
14070
|
// src/chat/runtime/turn-preparation.ts
|
|
14071
|
+
function hasPendingImageHydration(conversation) {
|
|
14072
|
+
return conversation.messages.some(
|
|
14073
|
+
(message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
|
|
14074
|
+
);
|
|
14075
|
+
}
|
|
12971
14076
|
function createPrepareTurnState(deps) {
|
|
12972
14077
|
return async function prepareTurnState(args) {
|
|
12973
14078
|
const existingState = await args.thread.state;
|
|
@@ -12985,14 +14090,8 @@ function createPrepareTurnState(deps) {
|
|
|
12985
14090
|
messageId: args.message.id,
|
|
12986
14091
|
messageCreatedAtMs: args.message.metadata.dateSent.getTime()
|
|
12987
14092
|
});
|
|
12988
|
-
const messageHasPotentialImageAttachment =
|
|
12989
|
-
|
|
12990
|
-
if (attachment.type === "image") {
|
|
12991
|
-
return true;
|
|
12992
|
-
}
|
|
12993
|
-
const mimeType = attachment.mimeType ?? "";
|
|
12994
|
-
return attachment.type === "file" && mimeType.startsWith("image/");
|
|
12995
|
-
}
|
|
14093
|
+
const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
|
|
14094
|
+
args.message.attachments
|
|
12996
14095
|
);
|
|
12997
14096
|
const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
|
|
12998
14097
|
const slackTs = getSlackMessageTs(args.message);
|
|
@@ -13017,7 +14116,8 @@ function createPrepareTurnState(deps) {
|
|
|
13017
14116
|
conversation,
|
|
13018
14117
|
incomingUserMessage
|
|
13019
14118
|
);
|
|
13020
|
-
|
|
14119
|
+
const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
|
|
14120
|
+
if (isVisionEnabled() && shouldHydrateVisionContext) {
|
|
13021
14121
|
await deps.hydrateConversationVisionContext(conversation, {
|
|
13022
14122
|
threadId: args.context.threadId,
|
|
13023
14123
|
channelId: args.context.channelId,
|
|
@@ -13113,7 +14213,7 @@ function createSlackRuntime(options) {
|
|
|
13113
14213
|
slackTs,
|
|
13114
14214
|
replied: false,
|
|
13115
14215
|
skippedReason: decision.reason,
|
|
13116
|
-
imagesHydrated:
|
|
14216
|
+
imagesHydrated: !hasPotentialImageAttachment(message.attachments)
|
|
13117
14217
|
}
|
|
13118
14218
|
});
|
|
13119
14219
|
conversation.processing.activeTurnId = void 0;
|
|
@@ -13397,6 +14497,166 @@ var JuniorChat = class extends Chat {
|
|
|
13397
14497
|
}
|
|
13398
14498
|
};
|
|
13399
14499
|
|
|
14500
|
+
// src/chat/slack/adapter.ts
|
|
14501
|
+
import {
|
|
14502
|
+
createSlackAdapter
|
|
14503
|
+
} from "@chat-adapter/slack";
|
|
14504
|
+
import {
|
|
14505
|
+
StreamingMarkdownRenderer
|
|
14506
|
+
} from "chat";
|
|
14507
|
+
var STREAM_BUFFER_SIZE = 64;
|
|
14508
|
+
var CLIENT_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.client_stream_patched");
|
|
14509
|
+
var ADAPTER_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.adapter_stream_patched");
|
|
14510
|
+
function assertSlackAdapterInternals(internals) {
|
|
14511
|
+
if (!internals.client || typeof internals.client.chatStream !== "function") {
|
|
14512
|
+
throw new Error("Slack adapter client does not expose chatStream()");
|
|
14513
|
+
}
|
|
14514
|
+
if (typeof internals.stream !== "function") {
|
|
14515
|
+
throw new Error("Slack adapter does not expose stream()");
|
|
14516
|
+
}
|
|
14517
|
+
if (typeof internals.decodeThreadId !== "function") {
|
|
14518
|
+
throw new Error("Slack adapter does not expose decodeThreadId()");
|
|
14519
|
+
}
|
|
14520
|
+
if (typeof internals.getToken !== "function") {
|
|
14521
|
+
throw new Error("Slack adapter does not expose getToken()");
|
|
14522
|
+
}
|
|
14523
|
+
if (!internals.logger || typeof internals.logger.debug !== "function" || typeof internals.logger.warn !== "function") {
|
|
14524
|
+
throw new Error("Slack adapter does not expose logger debug/warn methods");
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
function shouldEagerFlushPlainText(text) {
|
|
14528
|
+
return text.length > 0 && !text.includes("\n") && !/[`*~[\]|]/.test(text);
|
|
14529
|
+
}
|
|
14530
|
+
function getNextRenderableDelta(renderer, lastAppended) {
|
|
14531
|
+
const committable = renderer.getCommittableText();
|
|
14532
|
+
if (committable.startsWith(lastAppended)) {
|
|
14533
|
+
const delta = committable.slice(lastAppended.length);
|
|
14534
|
+
if (delta) {
|
|
14535
|
+
return { delta, nextAppended: committable };
|
|
14536
|
+
}
|
|
14537
|
+
}
|
|
14538
|
+
const rawText = renderer.getText();
|
|
14539
|
+
if (shouldEagerFlushPlainText(rawText) && rawText.startsWith(lastAppended) && rawText.length > lastAppended.length) {
|
|
14540
|
+
return {
|
|
14541
|
+
delta: rawText.slice(lastAppended.length),
|
|
14542
|
+
nextAppended: rawText
|
|
14543
|
+
};
|
|
14544
|
+
}
|
|
14545
|
+
return { delta: "", nextAppended: lastAppended };
|
|
14546
|
+
}
|
|
14547
|
+
function patchSlackClientStream(adapter) {
|
|
14548
|
+
const internals = adapter;
|
|
14549
|
+
const { client: client2 } = internals;
|
|
14550
|
+
if (client2[CLIENT_STREAM_PATCHED]) {
|
|
14551
|
+
return;
|
|
14552
|
+
}
|
|
14553
|
+
const originalChatStream = client2.chatStream.bind(client2);
|
|
14554
|
+
client2.chatStream = (params) => originalChatStream({
|
|
14555
|
+
...params,
|
|
14556
|
+
buffer_size: STREAM_BUFFER_SIZE
|
|
14557
|
+
});
|
|
14558
|
+
client2[CLIENT_STREAM_PATCHED] = true;
|
|
14559
|
+
}
|
|
14560
|
+
function patchSlackAdapterStream(adapter) {
|
|
14561
|
+
const internals = adapter;
|
|
14562
|
+
if (internals[ADAPTER_STREAM_PATCHED]) {
|
|
14563
|
+
return;
|
|
14564
|
+
}
|
|
14565
|
+
const originalStream = internals.stream.bind(adapter);
|
|
14566
|
+
internals.stream = async function(threadId, textStream, options) {
|
|
14567
|
+
if (!(options?.recipientUserId && options?.recipientTeamId)) {
|
|
14568
|
+
return originalStream(threadId, textStream, options);
|
|
14569
|
+
}
|
|
14570
|
+
const { channel, threadTs } = internals.decodeThreadId(threadId);
|
|
14571
|
+
internals.logger.debug("Slack: starting stream", { channel, threadTs });
|
|
14572
|
+
const token = internals.getToken();
|
|
14573
|
+
const streamer = internals.client.chatStream({
|
|
14574
|
+
channel,
|
|
14575
|
+
thread_ts: threadTs,
|
|
14576
|
+
recipient_user_id: options.recipientUserId,
|
|
14577
|
+
recipient_team_id: options.recipientTeamId,
|
|
14578
|
+
...options.taskDisplayMode ? { task_display_mode: options.taskDisplayMode } : {}
|
|
14579
|
+
});
|
|
14580
|
+
let first = true;
|
|
14581
|
+
let lastAppended = "";
|
|
14582
|
+
let structuredChunksSupported = true;
|
|
14583
|
+
const renderer = new StreamingMarkdownRenderer();
|
|
14584
|
+
const flushMarkdownDelta = async (delta) => {
|
|
14585
|
+
if (delta.length === 0) {
|
|
14586
|
+
return;
|
|
14587
|
+
}
|
|
14588
|
+
if (first) {
|
|
14589
|
+
await streamer.append({ markdown_text: delta, token, chunks: [] });
|
|
14590
|
+
first = false;
|
|
14591
|
+
return;
|
|
14592
|
+
}
|
|
14593
|
+
await streamer.append({ markdown_text: delta });
|
|
14594
|
+
};
|
|
14595
|
+
const flushText = async () => {
|
|
14596
|
+
const { delta, nextAppended } = getNextRenderableDelta(
|
|
14597
|
+
renderer,
|
|
14598
|
+
lastAppended
|
|
14599
|
+
);
|
|
14600
|
+
await flushMarkdownDelta(delta);
|
|
14601
|
+
lastAppended = nextAppended;
|
|
14602
|
+
};
|
|
14603
|
+
const sendStructuredChunk = async (chunk) => {
|
|
14604
|
+
if (!structuredChunksSupported) {
|
|
14605
|
+
return;
|
|
14606
|
+
}
|
|
14607
|
+
await flushText();
|
|
14608
|
+
try {
|
|
14609
|
+
if (first) {
|
|
14610
|
+
await streamer.append({ chunks: [chunk], token });
|
|
14611
|
+
first = false;
|
|
14612
|
+
return;
|
|
14613
|
+
}
|
|
14614
|
+
await streamer.append({ chunks: [chunk] });
|
|
14615
|
+
} catch (error) {
|
|
14616
|
+
structuredChunksSupported = false;
|
|
14617
|
+
internals.logger.warn(
|
|
14618
|
+
"Structured streaming chunk failed, falling back to text-only streaming. Ensure your Slack app manifest includes assistant_view, assistant:write scope, and @slack/web-api >= 7.14.0",
|
|
14619
|
+
{ chunkType: chunk.type, error }
|
|
14620
|
+
);
|
|
14621
|
+
}
|
|
14622
|
+
};
|
|
14623
|
+
const pushTextAndFlush = async (text) => {
|
|
14624
|
+
renderer.push(text);
|
|
14625
|
+
await flushText();
|
|
14626
|
+
};
|
|
14627
|
+
for await (const chunk of textStream) {
|
|
14628
|
+
if (typeof chunk === "string") {
|
|
14629
|
+
await pushTextAndFlush(chunk);
|
|
14630
|
+
} else if (chunk.type === "markdown_text") {
|
|
14631
|
+
await pushTextAndFlush(chunk.text);
|
|
14632
|
+
} else {
|
|
14633
|
+
await sendStructuredChunk(chunk);
|
|
14634
|
+
}
|
|
14635
|
+
}
|
|
14636
|
+
renderer.finish();
|
|
14637
|
+
await flushText();
|
|
14638
|
+
const result = await streamer.stop(
|
|
14639
|
+
options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
|
|
14640
|
+
);
|
|
14641
|
+
const messageTs = result.message?.ts ?? result.ts;
|
|
14642
|
+
internals.logger.debug("Slack: stream complete", { messageId: messageTs });
|
|
14643
|
+
return {
|
|
14644
|
+
id: messageTs,
|
|
14645
|
+
threadId,
|
|
14646
|
+
raw: result
|
|
14647
|
+
};
|
|
14648
|
+
};
|
|
14649
|
+
internals[ADAPTER_STREAM_PATCHED] = true;
|
|
14650
|
+
}
|
|
14651
|
+
function createJuniorSlackAdapter(config) {
|
|
14652
|
+
const adapter = createSlackAdapter(config);
|
|
14653
|
+
const internals = adapter;
|
|
14654
|
+
assertSlackAdapterInternals(internals);
|
|
14655
|
+
patchSlackClientStream(adapter);
|
|
14656
|
+
patchSlackAdapterStream(adapter);
|
|
14657
|
+
return adapter;
|
|
14658
|
+
}
|
|
14659
|
+
|
|
13400
14660
|
// src/chat/queue/thread-message-dispatcher.ts
|
|
13401
14661
|
function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
|
|
13402
14662
|
for (const attachment of message.attachments) {
|
|
@@ -13493,8 +14753,10 @@ async function handleSlashCommand(event) {
|
|
|
13493
14753
|
var productionBot;
|
|
13494
14754
|
var productionSlackRuntime;
|
|
13495
14755
|
function createProductionBot() {
|
|
14756
|
+
const logger = createChatSdkLogger();
|
|
13496
14757
|
return new JuniorChat({
|
|
13497
14758
|
userName: botConfig.userName,
|
|
14759
|
+
logger,
|
|
13498
14760
|
concurrency: {
|
|
13499
14761
|
strategy: "queue",
|
|
13500
14762
|
// The SDK's default queueEntryTtlMs is 90s, but Junior turns can
|
|
@@ -13513,7 +14775,8 @@ function createProductionBot() {
|
|
|
13513
14775
|
if (!signingSecret) {
|
|
13514
14776
|
throw new Error("SLACK_SIGNING_SECRET is required");
|
|
13515
14777
|
}
|
|
13516
|
-
return
|
|
14778
|
+
return createJuniorSlackAdapter({
|
|
14779
|
+
logger: logger.child("slack"),
|
|
13517
14780
|
signingSecret,
|
|
13518
14781
|
...botToken ? { botToken } : {},
|
|
13519
14782
|
...clientId ? { clientId } : {},
|
|
@@ -13648,6 +14911,32 @@ function isMessageChangedEnvelope(value) {
|
|
|
13648
14911
|
function textMentionsBot(text, botUserId) {
|
|
13649
14912
|
return text.includes(`<@${botUserId}>`);
|
|
13650
14913
|
}
|
|
14914
|
+
function getAttachmentType(mimeType) {
|
|
14915
|
+
if (mimeType?.startsWith("image/")) {
|
|
14916
|
+
return "image";
|
|
14917
|
+
}
|
|
14918
|
+
if (mimeType?.startsWith("video/")) {
|
|
14919
|
+
return "video";
|
|
14920
|
+
}
|
|
14921
|
+
if (mimeType?.startsWith("audio/")) {
|
|
14922
|
+
return "audio";
|
|
14923
|
+
}
|
|
14924
|
+
return "file";
|
|
14925
|
+
}
|
|
14926
|
+
function extractEditedMessageAttachments(files) {
|
|
14927
|
+
if (!files || files.length === 0) {
|
|
14928
|
+
return [];
|
|
14929
|
+
}
|
|
14930
|
+
return files.map((file) => ({
|
|
14931
|
+
type: getAttachmentType(file.mimetype),
|
|
14932
|
+
url: file.url_private_download ?? file.url_private,
|
|
14933
|
+
name: file.name,
|
|
14934
|
+
mimeType: file.mimetype,
|
|
14935
|
+
size: file.size,
|
|
14936
|
+
width: file.original_w,
|
|
14937
|
+
height: file.original_h
|
|
14938
|
+
}));
|
|
14939
|
+
}
|
|
13651
14940
|
function extractMessageChangedMention(body, botUserId, adapter) {
|
|
13652
14941
|
if (!isMessageChangedEnvelope(body)) return null;
|
|
13653
14942
|
const { event } = body;
|
|
@@ -13673,7 +14962,7 @@ function extractMessageChangedMention(body, botUserId, adapter) {
|
|
|
13673
14962
|
threadId,
|
|
13674
14963
|
text: newText,
|
|
13675
14964
|
isMention: true,
|
|
13676
|
-
attachments:
|
|
14965
|
+
attachments: extractEditedMessageAttachments(event.message.files),
|
|
13677
14966
|
metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
|
|
13678
14967
|
formatted: { type: "root", children: [] },
|
|
13679
14968
|
raw,
|
|
@@ -13726,6 +15015,7 @@ async function handleAuthenticatedSlackMessageChangedMention(args) {
|
|
|
13726
15015
|
if (!result) {
|
|
13727
15016
|
return false;
|
|
13728
15017
|
}
|
|
15018
|
+
rehydrateAttachmentFetchers(result.message);
|
|
13729
15019
|
args.bot.processMessage(
|
|
13730
15020
|
slackAdapter,
|
|
13731
15021
|
result.threadId,
|
|
@@ -13847,7 +15137,7 @@ async function handlePlatformWebhook(request, platform, waitUntil, bot = getProd
|
|
|
13847
15137
|
}
|
|
13848
15138
|
});
|
|
13849
15139
|
}
|
|
13850
|
-
async function
|
|
15140
|
+
async function POST2(request, platform, waitUntil) {
|
|
13851
15141
|
return handlePlatformWebhook(request, platform, waitUntil);
|
|
13852
15142
|
}
|
|
13853
15143
|
|
|
@@ -13900,8 +15190,11 @@ async function createApp(options) {
|
|
|
13900
15190
|
app.get("/api/oauth/callback/:provider", (c) => {
|
|
13901
15191
|
return GET5(c.req.raw, c.req.param("provider"), waitUntil);
|
|
13902
15192
|
});
|
|
15193
|
+
app.post("/api/internal/turn-resume", (c) => {
|
|
15194
|
+
return POST(c.req.raw, waitUntil);
|
|
15195
|
+
});
|
|
13903
15196
|
app.post("/api/webhooks/:platform", (c) => {
|
|
13904
|
-
return
|
|
15197
|
+
return POST2(c.req.raw, c.req.param("platform"), waitUntil);
|
|
13905
15198
|
});
|
|
13906
15199
|
return app;
|
|
13907
15200
|
}
|