@sentry/junior 0.72.0 → 0.73.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.
Files changed (78) hide show
  1. package/bin/junior.mjs +12 -10
  2. package/dist/app.d.ts +11 -2
  3. package/dist/app.js +936 -13042
  4. package/dist/chat/agent-dispatch/runner.d.ts +2 -0
  5. package/dist/chat/agent-dispatch/store.d.ts +2 -2
  6. package/dist/chat/agent-dispatch/types.d.ts +6 -3
  7. package/dist/chat/agent-dispatch/validation.d.ts +2 -3
  8. package/dist/chat/app/production.d.ts +8 -1
  9. package/dist/chat/app/services.d.ts +7 -0
  10. package/dist/chat/destination.d.ts +3 -1
  11. package/dist/chat/logging.d.ts +3 -0
  12. package/dist/chat/oauth-flow.d.ts +1 -1
  13. package/dist/chat/plugins/registry.d.ts +2 -0
  14. package/dist/chat/plugins/types.d.ts +2 -0
  15. package/dist/chat/prompt.d.ts +4 -1
  16. package/dist/chat/requester.d.ts +19 -12
  17. package/dist/chat/respond.d.ts +6 -2
  18. package/dist/chat/runtime/agent-continue-runner.d.ts +1 -1
  19. package/dist/chat/runtime/reply-executor.d.ts +3 -1
  20. package/dist/chat/runtime/slack-resume.d.ts +3 -3
  21. package/dist/chat/runtime/slack-runtime.d.ts +13 -3
  22. package/dist/chat/runtime/turn.d.ts +15 -1
  23. package/dist/chat/sandbox/egress-credentials.d.ts +5 -2
  24. package/dist/chat/sandbox/egress-policy.d.ts +5 -1
  25. package/dist/chat/sandbox/egress-proxy.d.ts +2 -0
  26. package/dist/chat/sandbox/egress-schemas.d.ts +4 -0
  27. package/dist/chat/sandbox/egress-session.d.ts +3 -1
  28. package/dist/chat/sandbox/egress-tracing.d.ts +7 -0
  29. package/dist/chat/sandbox/sandbox.d.ts +2 -0
  30. package/dist/chat/sandbox/session.d.ts +3 -2
  31. package/dist/chat/services/auth-pause-response.d.ts +1 -1
  32. package/dist/chat/services/auth-pause.d.ts +2 -1
  33. package/dist/chat/services/mcp-auth-orchestration.d.ts +1 -1
  34. package/dist/chat/services/message-actor-identity.d.ts +2 -2
  35. package/dist/chat/services/plugin-auth-orchestration.d.ts +9 -8
  36. package/dist/chat/services/turn-result.d.ts +3 -0
  37. package/dist/chat/slack/user.d.ts +2 -2
  38. package/dist/chat/task-execution/store.d.ts +1 -1
  39. package/dist/chat/tools/slack/canvas-tools.d.ts +3 -2
  40. package/dist/chat/tools/slack/channel-list-messages.d.ts +2 -2
  41. package/dist/chat/tools/slack/channel-post-message.d.ts +3 -2
  42. package/dist/chat/tools/slack/context.d.ts +15 -2
  43. package/dist/chat/tools/slack/message-add-reaction.d.ts +3 -2
  44. package/dist/chat/tools/slack/thread-read.d.ts +2 -2
  45. package/dist/chat/tools/types.d.ts +19 -19
  46. package/dist/{chunk-6GEYPE6T.js → chunk-3BYAPS6B.js} +30 -6
  47. package/dist/{chunk-GB3AL54K.js → chunk-7Q5YOUUT.js} +10 -2
  48. package/dist/{chunk-HNMUVGSR.js → chunk-AL5T52ZD.js} +6 -6
  49. package/dist/{chunk-PP7AGSBU.js → chunk-CYUI7JU5.js} +18 -8
  50. package/dist/{chunk-3FYPXHPL.js → chunk-DIMX5F3T.js} +1 -1
  51. package/dist/{chunk-4JXCSGSA.js → chunk-M4FLLXXD.js} +1 -1
  52. package/dist/chunk-OQSYYOLM.js +12787 -0
  53. package/dist/{chunk-55XEZFGD.js → chunk-OR6NQJ5E.js} +2 -2
  54. package/dist/{chunk-VLIO6RQR.js → chunk-RY6AL5C7.js} +4 -4
  55. package/dist/chunk-SJHUF3DP.js +43 -0
  56. package/dist/{chunk-ICKIDP7G.js → chunk-UOTZ3EEQ.js} +1 -1
  57. package/dist/{chunk-VSNA5KAB.js → chunk-UZVHXZ7V.js} +1276 -1481
  58. package/dist/{chunk-XC33FJZN.js → chunk-V4VYUY4A.js} +25 -4
  59. package/dist/{chunk-ZJQPA67D.js → chunk-WS2EG3GW.js} +224 -224
  60. package/dist/chunk-ZDA2HYX5.js +275 -0
  61. package/dist/cli/chat.js +205 -0
  62. package/dist/cli/check.js +9 -10
  63. package/dist/cli/run.js +10 -2
  64. package/dist/cli/snapshot-warmup.js +7 -7
  65. package/dist/cli/upgrade.js +8 -8
  66. package/dist/deployment.d.ts +4 -0
  67. package/dist/handlers/agent-dispatch.d.ts +6 -1
  68. package/dist/handlers/mcp-oauth-callback.d.ts +6 -1
  69. package/dist/handlers/oauth-callback.d.ts +6 -1
  70. package/dist/handlers/sandbox-egress-proxy.d.ts +2 -0
  71. package/dist/handlers/webhooks.d.ts +4 -2
  72. package/dist/instrumentation.js +17 -2
  73. package/dist/nitro.js +9 -10
  74. package/dist/reporting.js +19 -17
  75. package/dist/runner-LMAM4OGD.js +259 -0
  76. package/package.json +3 -3
  77. package/dist/chunk-6YY4Q3D4.js +0 -12
  78. package/dist/chunk-Z3YD6NHK.js +0 -12
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  recordConversationActivity
3
- } from "./chunk-HNMUVGSR.js";
3
+ } from "./chunk-AL5T52ZD.js";
4
4
  import {
5
5
  isConversationChannel,
6
6
  isConversationScopedChannel,
7
7
  isDmChannel,
8
8
  normalizeSlackConversationId,
9
9
  parseDestination
10
- } from "./chunk-XC33FJZN.js";
10
+ } from "./chunk-V4VYUY4A.js";
11
11
  import {
12
12
  SANDBOX_DATA_ROOT,
13
13
  SANDBOX_WORKSPACE_ROOT,
@@ -21,29 +21,24 @@ import {
21
21
  import {
22
22
  getConnectedStateContext,
23
23
  getStateAdapter
24
- } from "./chunk-3FYPXHPL.js";
24
+ } from "./chunk-DIMX5F3T.js";
25
25
  import {
26
26
  TURN_CONTEXT_TAG,
27
27
  botConfig,
28
- getChatConfig,
29
- parseSlackThreadId
30
- } from "./chunk-ZJQPA67D.js";
28
+ getChatConfig
29
+ } from "./chunk-WS2EG3GW.js";
31
30
  import {
32
31
  isActorUserId,
33
32
  parseActorUserId,
34
33
  parseStoredSlackRequester,
35
34
  storedSlackRequesterSchema
36
- } from "./chunk-PP7AGSBU.js";
35
+ } from "./chunk-CYUI7JU5.js";
37
36
  import {
38
37
  isRecord,
39
38
  logException,
40
39
  logInfo,
41
- logWarn,
42
- toOptionalNumber
43
- } from "./chunk-6GEYPE6T.js";
44
- import {
45
- sentry_exports
46
- } from "./chunk-Z3YD6NHK.js";
40
+ logWarn
41
+ } from "./chunk-3BYAPS6B.js";
47
42
 
48
43
  // src/chat/plugins/logging.ts
49
44
  function createAgentPluginLogger(plugin) {
@@ -169,6 +164,23 @@ function createPluginState(plugin, options) {
169
164
  };
170
165
  }
171
166
 
167
+ // src/chat/tools/slack/context.ts
168
+ function getSlackToolContext(context) {
169
+ if (context.source.platform !== "slack") {
170
+ return void 0;
171
+ }
172
+ return {
173
+ destination: context.destination?.platform === "slack" ? context.destination : void 0,
174
+ source: context.source,
175
+ requester: context.requester?.platform === "slack" ? context.requester : void 0,
176
+ destinationChannelId: context.destination?.platform === "slack" ? context.destination.channelId : void 0,
177
+ messageTs: context.source.messageTs,
178
+ sourceChannelId: context.source.channelId,
179
+ teamId: context.source.teamId,
180
+ threadTs: context.source.threadTs
181
+ };
182
+ }
183
+
172
184
  // src/chat/credentials/subject.ts
173
185
  import { createHmac, timingSafeEqual } from "crypto";
174
186
  var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
@@ -360,29 +372,43 @@ function getAgentPluginTools(context) {
360
372
  }
361
373
  const log = createAgentPluginLogger(plugin.name);
362
374
  const destination = context.destination;
363
- const credentialSubject = createSlackDirectCredentialSubject({
364
- channelId: context.channelId,
365
- teamId: context.teamId,
366
- userId: context.requester?.userId
367
- });
368
- const pluginCapabilities = resolveChannelCapabilities(context.channelId);
369
- const pluginTools = hook({
375
+ const slackToolContext = getSlackToolContext(context);
376
+ const credentialSubject = slackToolContext ? createSlackDirectCredentialSubject({
377
+ channelId: slackToolContext.sourceChannelId,
378
+ teamId: slackToolContext.teamId,
379
+ userId: slackToolContext.requester?.userId
380
+ }) : void 0;
381
+ const slackContext = slackToolContext ? {
382
+ channelCapabilities: resolveChannelCapabilities(
383
+ slackToolContext.sourceChannelId
384
+ ),
385
+ ...credentialSubject ? { credentialSubject } : {}
386
+ } : void 0;
387
+ const pluginContext = context.source.platform === "slack" ? {
370
388
  plugin: { name: plugin.name },
371
389
  log,
372
- requester: context.requester,
373
- channelCapabilities: pluginCapabilities,
374
- channelId: context.channelId,
390
+ requester: context.requester?.platform === "slack" ? context.requester : void 0,
375
391
  conversationId: context.conversationId,
376
- ...credentialSubject ? { credentialSubject } : {},
377
- ...destination ? { destination } : {},
378
- teamId: context.teamId,
379
- messageTs: context.messageTs,
380
- threadTs: context.threadTs,
392
+ destination: destination?.platform === "slack" ? destination : void 0,
393
+ slack: slackContext,
394
+ source: context.source,
381
395
  userText: context.userText,
382
396
  state: createPluginState(plugin.name, {
383
397
  legacyStatePrefixes: plugin.legacyStatePrefixes
384
398
  })
385
- });
399
+ } : {
400
+ plugin: { name: plugin.name },
401
+ log,
402
+ requester: context.requester?.platform === "local" ? context.requester : void 0,
403
+ conversationId: context.conversationId,
404
+ destination: destination?.platform === "local" ? destination : void 0,
405
+ source: context.source,
406
+ userText: context.userText,
407
+ state: createPluginState(plugin.name, {
408
+ legacyStatePrefixes: plugin.legacyStatePrefixes
409
+ })
410
+ };
411
+ const pluginTools = hook(pluginContext);
386
412
  for (const [name, tool] of Object.entries(pluginTools)) {
387
413
  if (!AGENT_PLUGIN_TOOL_NAME_RE.test(name)) {
388
414
  throw new Error(
@@ -818,1345 +844,1357 @@ function createAgentPluginHookRunner(input = {}) {
818
844
  };
819
845
  }
820
846
 
821
- // src/handlers/health.ts
822
- function GET() {
823
- return Response.json({
824
- status: "ok",
825
- service: "junior",
826
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
827
- });
828
- }
829
-
830
- // src/chat/xml.ts
831
- function escapeXml(value) {
832
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
847
+ // src/chat/state/session-log.ts
848
+ import { isDeepStrictEqual } from "util";
849
+ import { z } from "zod";
850
+ var AGENT_SESSION_LOG_PREFIX = "junior:agent-session-log";
851
+ var AGENT_SESSION_LOG_SCHEMA_VERSION = 1;
852
+ var INITIAL_SESSION_ID = "session_0";
853
+ var SESSION_ID_PREFIX = "session_";
854
+ var piMessageSchema = z.object({
855
+ role: z.string()
856
+ }).passthrough().transform((value) => value);
857
+ var piMessageEntrySchema = z.object({
858
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
859
+ type: z.literal("pi_message"),
860
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
861
+ message: piMessageSchema,
862
+ requester: storedSlackRequesterSchema.optional()
863
+ });
864
+ var projectionResetEntrySchema = z.object({
865
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
866
+ type: z.literal("projection_reset"),
867
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
868
+ messages: z.array(piMessageSchema),
869
+ requester: storedSlackRequesterSchema.optional()
870
+ });
871
+ var requesterRecordedEntrySchema = z.object({
872
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
873
+ type: z.literal("requester_recorded"),
874
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
875
+ requester: storedSlackRequesterSchema
876
+ });
877
+ var mcpProviderConnectedEntrySchema = z.object({
878
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
879
+ type: z.literal("mcp_provider_connected"),
880
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
881
+ provider: z.string().min(1)
882
+ });
883
+ var authorizationKindSchema = z.union([
884
+ z.literal("plugin"),
885
+ z.literal("mcp")
886
+ ]);
887
+ var authorizationRequestedEntrySchema = z.object({
888
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
889
+ type: z.literal("authorization_requested"),
890
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
891
+ createdAtMs: z.number().int().nonnegative(),
892
+ kind: authorizationKindSchema,
893
+ provider: z.string().min(1),
894
+ requesterId: z.string().min(1),
895
+ authorizationId: z.string().min(1),
896
+ delivery: z.union([
897
+ z.literal("private_link_sent"),
898
+ z.literal("private_link_reused")
899
+ ])
900
+ });
901
+ var authorizationCompletedEntrySchema = z.object({
902
+ schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
903
+ type: z.literal("authorization_completed"),
904
+ sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
905
+ createdAtMs: z.number().int().nonnegative(),
906
+ kind: authorizationKindSchema,
907
+ provider: z.string().min(1),
908
+ requesterId: z.string().min(1),
909
+ authorizationId: z.string().min(1)
910
+ });
911
+ var sessionLogEntrySchema = z.discriminatedUnion("type", [
912
+ piMessageEntrySchema,
913
+ projectionResetEntrySchema,
914
+ requesterRecordedEntrySchema,
915
+ mcpProviderConnectedEntrySchema,
916
+ authorizationRequestedEntrySchema,
917
+ authorizationCompletedEntrySchema
918
+ ]);
919
+ function key(scope) {
920
+ const prefix = getChatConfig().state.keyPrefix;
921
+ return [
922
+ ...prefix ? [prefix] : [],
923
+ AGENT_SESSION_LOG_PREFIX,
924
+ scope.conversationId
925
+ ].join(":");
833
926
  }
834
-
835
- // src/chat/prompt.ts
836
- import fs from "fs";
837
- import path from "path";
838
-
839
- // src/chat/interruption-marker.ts
840
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
841
- function getInterruptionMarker() {
842
- return INTERRUPTED_MARKER;
927
+ function rawKey(scope) {
928
+ return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
843
929
  }
844
-
845
- // src/chat/slack/status-format.ts
846
- var SLACK_STATUS_MAX_LENGTH = 50;
847
- function truncateStatusText(text) {
848
- const trimmed = text.trim();
849
- if (!trimmed) {
850
- return "";
851
- }
852
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
853
- return trimmed;
854
- }
855
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
930
+ function normalizeMessageCount(value) {
931
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
856
932
  }
857
-
858
- // src/chat/slack/mrkdwn.ts
859
- function readInlineCodeSpan(line, start) {
860
- if (line[start] !== "`") {
861
- return void 0;
862
- }
863
- let n = 1;
864
- while (line[start + n] === "`") {
865
- n++;
866
- }
867
- const marker = "`".repeat(n);
868
- let search = start + n;
869
- while (search < line.length) {
870
- const close = line.indexOf(marker, search);
871
- if (close === -1) {
872
- return void 0;
873
- }
874
- const after = close + n;
875
- if (line[after] !== "`") {
876
- return { text: line.slice(start, after), end: after };
933
+ function countMatchingPrefix(left, right) {
934
+ const limit = Math.min(left.length, right.length);
935
+ for (let index = 0; index < limit; index += 1) {
936
+ if (!isDeepStrictEqual(left[index], right[index])) {
937
+ return index;
877
938
  }
878
- search = after + 1;
879
939
  }
880
- return void 0;
940
+ return limit;
881
941
  }
882
- function readExistingSlackAngleToken(line, start) {
883
- if (line[start] !== "<") {
884
- return void 0;
885
- }
886
- const close = line.indexOf(">", start + 1);
887
- if (close === -1) {
888
- return void 0;
889
- }
890
- const body = line.slice(start + 1, close);
891
- if (/^(?:https?:\/\/|@|#|!)/.test(body)) {
892
- return { text: line.slice(start, close + 1), end: close + 1 };
893
- }
894
- return void 0;
942
+ function entrySessionId(entry) {
943
+ return entry.sessionId ?? INITIAL_SESSION_ID;
895
944
  }
896
- function readMarkdownLink(line, start) {
897
- if (line[start] !== "[") {
898
- return void 0;
899
- }
900
- const labelEnd = line.indexOf("](", start + 1);
901
- if (labelEnd === -1) {
902
- return void 0;
903
- }
904
- const destStart = labelEnd + 2;
905
- if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) {
906
- return void 0;
907
- }
908
- const closeParens = line.indexOf(")", destStart);
909
- if (closeParens === -1) {
910
- return void 0;
945
+ function latestProjectionResetIndex(entries) {
946
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
947
+ if (entries[index]?.type === "projection_reset") {
948
+ return index;
949
+ }
911
950
  }
912
- return { text: line.slice(start, closeParens + 1), end: closeParens + 1 };
951
+ return -1;
913
952
  }
914
- function hasUnmatchedClosingParen(text) {
915
- let balance = 0;
916
- for (const ch of text) {
917
- if (ch === "(") balance++;
918
- else if (ch === ")") balance--;
953
+ function currentSessionId(entries) {
954
+ const resetIndex = latestProjectionResetIndex(entries);
955
+ if (resetIndex < 0) {
956
+ return INITIAL_SESSION_ID;
919
957
  }
920
- return balance < 0;
958
+ return entrySessionId(entries[resetIndex]);
921
959
  }
922
- function readBareUrl(line, start) {
923
- let end = start;
924
- while (end < line.length) {
925
- const ch = line[end];
926
- if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === '"' || ch === "`" || ch === "|" || ch === "*") {
927
- break;
960
+ function nextSessionId(entries) {
961
+ const resetCount = entries.filter(
962
+ (entry) => entry.type === "projection_reset"
963
+ ).length;
964
+ return `${SESSION_ID_PREFIX}${resetCount + 1}`;
965
+ }
966
+ function projectionEntries(entries, sessionId) {
967
+ if (sessionId) {
968
+ const sessionEntries = [];
969
+ let started = false;
970
+ for (const entry of entries) {
971
+ const entryId = entrySessionId(entry);
972
+ if (!started) {
973
+ if (entryId !== sessionId) {
974
+ continue;
975
+ }
976
+ started = true;
977
+ } else if (entry.type === "projection_reset" && entryId !== sessionId) {
978
+ break;
979
+ }
980
+ if (entryId === sessionId) {
981
+ sessionEntries.push(entry);
982
+ }
928
983
  }
929
- end++;
930
- }
931
- if (end === start) {
932
- return void 0;
933
- }
934
- let raw = line.slice(start, end);
935
- let suffix = "";
936
- const peel = () => {
937
- suffix = raw.slice(-1) + suffix;
938
- raw = raw.slice(0, -1);
939
- };
940
- const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
941
- while (raw.length > 0 && shouldPeel()) {
942
- peel();
943
- }
944
- if (!/^https?:\/\/.+/.test(raw)) {
945
- return void 0;
984
+ return sessionEntries;
946
985
  }
947
- return { url: raw, suffix, end };
986
+ const resetIndex = latestProjectionResetIndex(entries);
987
+ const startIndex = resetIndex < 0 ? 0 : resetIndex;
988
+ const currentId = resetIndex < 0 ? INITIAL_SESSION_ID : entrySessionId(entries[resetIndex]);
989
+ return entries.slice(startIndex).filter((entry) => entrySessionId(entry) === currentId);
948
990
  }
949
- function wrapBareUrlsOnLine(line) {
950
- let result = "";
951
- let i = 0;
952
- while (i < line.length) {
953
- const codeSpan = readInlineCodeSpan(line, i);
954
- if (codeSpan) {
955
- result += codeSpan.text;
956
- i = codeSpan.end;
957
- continue;
958
- }
959
- const angleToken = readExistingSlackAngleToken(line, i);
960
- if (angleToken) {
961
- result += angleToken.text;
962
- i = angleToken.end;
963
- continue;
964
- }
965
- const mdLink = readMarkdownLink(line, i);
966
- if (mdLink) {
967
- result += mdLink.text;
968
- i = mdLink.end;
969
- continue;
970
- }
971
- if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
972
- const parsed = readBareUrl(line, i);
973
- if (parsed) {
974
- result += `<${parsed.url}>${parsed.suffix}`;
975
- i = parsed.end;
976
- continue;
977
- }
978
- }
979
- result += line[i];
980
- i++;
981
- }
982
- return result;
983
- }
984
- function wrapBareUrls(text) {
985
- const lines = text.split("\n");
986
- const out = [];
987
- let inCodeBlock = false;
988
- for (const line of lines) {
989
- if (line.trimStart().startsWith("```")) {
990
- inCodeBlock = !inCodeBlock;
991
- out.push(line);
992
- continue;
993
- }
994
- out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
995
- }
996
- return out.join("\n");
997
- }
998
- function ensureBlockSpacing(text) {
999
- const codeBlockPattern = /^```/;
1000
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
1001
- const lines = text.split("\n");
1002
- const result = [];
1003
- let inCodeBlock = false;
1004
- for (let i = 0; i < lines.length; i++) {
1005
- const line = lines[i];
1006
- const isCodeFence = codeBlockPattern.test(line.trimStart());
1007
- if (isCodeFence) {
1008
- if (!inCodeBlock) {
1009
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
1010
- if (prev2 !== void 0 && prev2.trim() !== "") {
1011
- result.push("");
1012
- }
1013
- }
1014
- inCodeBlock = !inCodeBlock;
1015
- result.push(line);
1016
- continue;
1017
- }
1018
- if (inCodeBlock) {
1019
- result.push(line);
1020
- continue;
1021
- }
1022
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
1023
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
1024
- result.push("");
1025
- }
1026
- result.push(line);
991
+ function findLastIndex(values, predicate) {
992
+ for (let index = values.length - 1; index >= 0; index -= 1) {
993
+ if (predicate(values[index])) return index;
1027
994
  }
1028
- return result.join("\n");
1029
- }
1030
- function normalizeSlackReplyMarkdown(text) {
1031
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1032
- normalized = wrapBareUrls(normalized);
1033
- normalized = ensureBlockSpacing(normalized);
1034
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
995
+ return -1;
1035
996
  }
1036
- function normalizeSlackStatusText(text) {
1037
- const trimmed = text.trim();
1038
- if (!trimmed) {
1039
- return "";
1040
- }
1041
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
997
+ function piEntry(message, sessionId, requester) {
998
+ return {
999
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1000
+ type: "pi_message",
1001
+ sessionId,
1002
+ message,
1003
+ ...requester ? { requester } : {}
1004
+ };
1042
1005
  }
1043
-
1044
- // src/chat/slack/output.ts
1045
- var MAX_INLINE_CHARS = 2200;
1046
- var MAX_INLINE_LINES = 45;
1047
- var CONTINUED_MARKER = "\n\n[Continued below]";
1048
- function countSlackLines(text) {
1049
- if (!text) {
1050
- return 0;
1051
- }
1052
- return text.split("\n").length;
1006
+ function resetEntry(messages, sessionId, requester) {
1007
+ return {
1008
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1009
+ type: "projection_reset",
1010
+ sessionId,
1011
+ messages,
1012
+ ...requester ? { requester } : {}
1013
+ };
1053
1014
  }
1054
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1055
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
1015
+ function requesterRecordedEntry(requester, sessionId) {
1016
+ return {
1017
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1018
+ type: "requester_recorded",
1019
+ sessionId,
1020
+ requester
1021
+ };
1056
1022
  }
1057
- function findSplitIndex(text, maxChars) {
1058
- if (text.length <= maxChars) {
1059
- return text.length;
1060
- }
1061
- const bounded = text.slice(0, maxChars);
1062
- const newlineIndex = bounded.lastIndexOf("\n");
1063
- if (newlineIndex > 0) {
1064
- return newlineIndex;
1065
- }
1066
- const spaceIndex = bounded.lastIndexOf(" ");
1067
- if (spaceIndex > 0) {
1068
- return spaceIndex;
1069
- }
1070
- return maxChars;
1023
+ function mcpProviderConnectedEntry(provider, sessionId) {
1024
+ return {
1025
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1026
+ type: "mcp_provider_connected",
1027
+ sessionId,
1028
+ provider
1029
+ };
1071
1030
  }
1072
- function splitByLineBudget(text, maxLines) {
1073
- if (maxLines <= 0) {
1074
- return "";
1075
- }
1076
- const lines = text.split("\n");
1077
- if (lines.length <= maxLines) {
1078
- return text;
1079
- }
1080
- return lines.slice(0, maxLines).join("\n");
1031
+ function authorizationObservationMessage(entry) {
1032
+ const label = entry.kind === "mcp" ? "MCP authorization" : "Authorization";
1033
+ return {
1034
+ role: "user",
1035
+ content: [
1036
+ {
1037
+ type: "text",
1038
+ text: `${label} completed for provider "${entry.provider}". Continue the blocked request and retry the provider operation if needed.`
1039
+ }
1040
+ ],
1041
+ timestamp: entry.createdAtMs
1042
+ };
1081
1043
  }
1082
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1044
+ function authorizationRequestedEntry(args) {
1083
1045
  return {
1084
- maxChars: Math.max(1, maxChars - suffix.length),
1085
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1046
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1047
+ type: "authorization_requested",
1048
+ sessionId: args.sessionId,
1049
+ createdAtMs: args.createdAtMs,
1050
+ kind: args.kind,
1051
+ provider: args.provider,
1052
+ requesterId: args.requesterId,
1053
+ authorizationId: args.authorizationId,
1054
+ delivery: args.delivery
1086
1055
  };
1087
1056
  }
1088
- function forceSplitBudget(text, budget) {
1089
- const lineCount = countSlackLines(text);
1057
+ function authorizationCompletedEntry(args) {
1090
1058
  return {
1091
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1092
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1059
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1060
+ type: "authorization_completed",
1061
+ sessionId: args.sessionId,
1062
+ createdAtMs: args.createdAtMs,
1063
+ kind: args.kind,
1064
+ provider: args.provider,
1065
+ requesterId: args.requesterId,
1066
+ authorizationId: args.authorizationId
1093
1067
  };
1094
1068
  }
1095
- function getFenceContinuation(text) {
1096
- let open;
1097
- for (const line of text.split("\n")) {
1098
- const trimmed = line.trimStart();
1099
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1100
- if (!openerMatch) {
1069
+ function decode(value) {
1070
+ if (typeof value === "string") {
1071
+ return decode(JSON.parse(value));
1072
+ }
1073
+ const parsed = sessionLogEntrySchema.safeParse(value);
1074
+ if (parsed.success) {
1075
+ return parsed.data;
1076
+ }
1077
+ return piEntry(piMessageSchema.parse(value), INITIAL_SESSION_ID);
1078
+ }
1079
+ function project(entries, sessionId) {
1080
+ let messages = [];
1081
+ let requester;
1082
+ for (const entry of projectionEntries(entries, sessionId)) {
1083
+ if (entry.type === "pi_message") {
1084
+ messages.push(entry.message);
1085
+ if (entry.message.role === "user" && entry.requester) {
1086
+ requester = entry.requester;
1087
+ }
1101
1088
  continue;
1102
1089
  }
1103
- if (!open) {
1104
- open = {
1105
- fence: openerMatch[1],
1106
- openerLine: trimmed
1107
- };
1090
+ if (entry.type === "requester_recorded") {
1091
+ requester = entry.requester;
1108
1092
  continue;
1109
1093
  }
1110
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1111
- open = void 0;
1094
+ if (entry.type === "authorization_completed") {
1095
+ messages.push(authorizationObservationMessage(entry));
1096
+ continue;
1097
+ }
1098
+ if (entry.type === "mcp_provider_connected" || entry.type === "authorization_requested") {
1099
+ continue;
1100
+ }
1101
+ messages = [...entry.messages];
1102
+ if (entry.requester) {
1103
+ requester = entry.requester;
1112
1104
  }
1113
1105
  }
1114
- if (!open) {
1115
- return null;
1116
- }
1117
- return {
1118
- closeSuffix: text.endsWith("\n") ? open.fence : `
1119
- ${open.fence}`,
1120
- reopenPrefix: `${open.openerLine}
1121
- `
1122
- };
1106
+ return { messages, requester };
1123
1107
  }
1124
- function appendSlackSuffix(text, marker) {
1125
- const carryover = getFenceContinuation(text);
1126
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1108
+ function projectMessages(entries, sessionId) {
1109
+ return project(entries, sessionId).messages;
1127
1110
  }
1128
- function stripTrailingContinuationMarker(text) {
1129
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1130
- }
1131
- function takeSlackContinuationChunk(text, budget) {
1132
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1133
- if (!rest) {
1134
- ({ prefix, rest } = takeSlackInlinePrefix(
1135
- text,
1136
- forceSplitBudget(text, budget)
1137
- ));
1138
- }
1139
- let carryover = rest ? getFenceContinuation(prefix) : null;
1140
- if (!carryover) {
1141
- return { prefix, rest };
1142
- }
1143
- const carryoverBudget = reserveInlineBudgetForSuffix(
1144
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
1145
- );
1146
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1147
- if (!rest) {
1148
- ({ prefix, rest } = takeSlackInlinePrefix(
1149
- text,
1150
- forceSplitBudget(text, carryoverBudget)
1151
- ));
1152
- }
1153
- carryover = rest ? getFenceContinuation(prefix) : null;
1154
- if (!carryover) {
1155
- return { prefix, rest };
1111
+ function connectedMcpProviders(entries, sessionId) {
1112
+ const providers = /* @__PURE__ */ new Set();
1113
+ for (const entry of projectionEntries(entries, sessionId)) {
1114
+ if (entry.type === "mcp_provider_connected") {
1115
+ providers.add(entry.provider);
1116
+ }
1156
1117
  }
1157
- return {
1158
- prefix,
1159
- rest: `${carryover.reopenPrefix}${rest}`
1160
- };
1118
+ return [...providers].sort((left, right) => left.localeCompare(right));
1161
1119
  }
1162
- function takeSlackContinuationPrefix(text, options) {
1163
- const budget = {
1164
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1165
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1166
- };
1167
- const { prefix, rest } = (() => {
1168
- if (options?.forceSplit) {
1169
- return takeSlackContinuationChunk(text, budget);
1120
+ function commitEntries(existingMessages, nextMessages, sessionId, entries, existingRequester, requester) {
1121
+ const matchingPrefix = countMatchingPrefix(existingMessages, nextMessages);
1122
+ if (matchingPrefix === existingMessages.length) {
1123
+ const newMessages = nextMessages.slice(matchingPrefix);
1124
+ if (newMessages.length === 0 && requester && !isDeepStrictEqual(existingRequester, requester)) {
1125
+ return [requesterRecordedEntry(requester, sessionId)];
1170
1126
  }
1171
- const initial = takeSlackInlinePrefix(text, budget);
1172
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1173
- })();
1127
+ const requesterIndex = requester ? findLastIndex(newMessages, (m) => m.role === "user") : -1;
1128
+ return newMessages.map(
1129
+ (message, index) => piEntry(
1130
+ message,
1131
+ sessionId,
1132
+ index === requesterIndex ? requester : void 0
1133
+ )
1134
+ );
1135
+ }
1136
+ return [
1137
+ resetEntry(
1138
+ nextMessages,
1139
+ nextSessionId(entries),
1140
+ requester ?? existingRequester
1141
+ )
1142
+ ];
1143
+ }
1144
+ function redisStore(redisStateAdapter) {
1145
+ const client = redisStateAdapter.getClient();
1174
1146
  return {
1175
- prefix,
1176
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1177
- rest
1147
+ async append({ entries, scope, ttlMs }) {
1148
+ const listKey = key(scope);
1149
+ if (entries.length > 0) {
1150
+ await client.rPush(
1151
+ listKey,
1152
+ entries.map((entry) => JSON.stringify(entry))
1153
+ );
1154
+ }
1155
+ await client.pExpire(listKey, Math.max(1, ttlMs));
1156
+ },
1157
+ async read(scope) {
1158
+ const values = await client.lRange(key(scope), 0, -1);
1159
+ return values.map(decode);
1160
+ }
1178
1161
  };
1179
1162
  }
1180
- function takeSlackInlinePrefix(text, options) {
1181
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1182
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1183
- const normalized = text.replace(/\r\n?/g, "\n");
1184
- if (!normalized) {
1185
- return { prefix: "", rest: "" };
1186
- }
1187
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1188
- return { prefix: normalized, rest: "" };
1189
- }
1190
- const lineBounded = splitByLineBudget(normalized, maxLines);
1191
- const cutIndex = findSplitIndex(lineBounded, maxChars);
1192
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1193
- if (prefix) {
1194
- return {
1195
- prefix,
1196
- rest: normalized.slice(prefix.length).trimStart()
1197
- };
1198
- }
1199
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1163
+ function stateStore() {
1164
+ const stateAdapter = getStateAdapter();
1200
1165
  return {
1201
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1202
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1166
+ async append({ entries, scope, ttlMs }) {
1167
+ const listKey = rawKey(scope);
1168
+ for (const entry of entries) {
1169
+ await stateAdapter.appendToList(listKey, entry, {
1170
+ ttlMs: Math.max(1, ttlMs)
1171
+ });
1172
+ }
1173
+ },
1174
+ async read(scope) {
1175
+ const values = await stateAdapter.getList(rawKey(scope));
1176
+ return values.map(decode);
1177
+ }
1203
1178
  };
1204
1179
  }
1205
- function splitSlackReplyText(text, options) {
1206
- const normalized = normalizeSlackReplyMarkdown(text);
1207
- if (!normalized) {
1208
- return [];
1209
- }
1210
- const chunks = [];
1211
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1212
- let remaining = normalized;
1213
- while (remaining) {
1214
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1215
- if (fitsFinalChunk) {
1216
- chunks.push(
1217
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1218
- );
1219
- break;
1220
- }
1221
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1222
- ...continuationBudget,
1223
- forceSplit: true
1224
- });
1225
- chunks.push(renderedPrefix);
1226
- remaining = rest;
1227
- }
1228
- if (chunks.length === 2) {
1229
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1180
+ async function defaultStore() {
1181
+ const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
1182
+ if (redisStateAdapter) {
1183
+ return redisStore(redisStateAdapter);
1230
1184
  }
1231
- return chunks;
1185
+ await stateAdapter.connect();
1186
+ return stateStore();
1232
1187
  }
1233
- function getSlackContinuationBudget() {
1234
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1188
+ async function loadEntries(args) {
1189
+ const store = args.store ?? await defaultStore();
1190
+ return await store.read(args);
1235
1191
  }
1236
- function buildSlackOutputMessage(text, files) {
1237
- const normalized = normalizeSlackReplyMarkdown(text);
1238
- const fileCount = files?.length ?? 0;
1239
- if (!normalized) {
1240
- if (fileCount > 0) {
1241
- return {
1242
- raw: "",
1243
- files
1244
- };
1245
- }
1246
- throw new Error(
1247
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1248
- );
1192
+ async function loadMessages(args) {
1193
+ const messageCount = normalizeMessageCount(args.messageCount);
1194
+ if (messageCount === 0) {
1195
+ return [];
1249
1196
  }
1250
- return {
1251
- markdown: normalized,
1252
- files
1253
- };
1197
+ const store = args.store ?? await defaultStore();
1198
+ const messages = projectMessages(await store.read(args), args.sessionId);
1199
+ return messages.length >= messageCount ? messages.slice(0, messageCount) : void 0;
1254
1200
  }
1255
- var slackOutputPolicy = {
1256
- maxInlineChars: MAX_INLINE_CHARS,
1257
- maxInlineLines: MAX_INLINE_LINES
1258
- };
1259
-
1260
- // src/chat/prompt.ts
1261
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1262
- function getLoggedMarkdownFiles() {
1263
- const globalState = globalThis;
1264
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
1265
- return globalState.__juniorLoggedMarkdownFiles;
1201
+ async function loadProjection(args) {
1202
+ const store = args.store ?? await defaultStore();
1203
+ return project(await store.read(args), args.sessionId).messages;
1266
1204
  }
1267
- function loadOptionalMarkdownFile(candidates, fileName) {
1268
- for (const resolved of candidates) {
1269
- try {
1270
- const raw = fs.readFileSync(resolved, "utf8").trim();
1271
- if (raw.length > 0) {
1272
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
1273
- const logKey = `${fileName}:${resolved}`;
1274
- if (!loggedMarkdownFiles.has(logKey)) {
1275
- loggedMarkdownFiles.add(logKey);
1276
- logInfo(
1277
- `${fileName.toLowerCase()}_loaded`,
1278
- {},
1279
- {
1280
- "file.path": resolved
1281
- },
1282
- `Loaded ${fileName}`
1283
- );
1284
- }
1285
- return raw;
1286
- }
1287
- } catch {
1288
- continue;
1289
- }
1290
- }
1291
- return null;
1205
+ async function loadConnectedMcpProviders(args) {
1206
+ return connectedMcpProviders(await loadEntries(args));
1292
1207
  }
1293
- function loadSoul() {
1294
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1295
- if (soul) {
1296
- return soul;
1208
+ async function recordMcpProviderConnected(args) {
1209
+ const store = args.store ?? await defaultStore();
1210
+ const entries = await store.read(args);
1211
+ const sessionId = currentSessionId(entries);
1212
+ if (connectedMcpProviders(entries).includes(args.provider)) {
1213
+ return;
1297
1214
  }
1298
- logWarn(
1299
- "soul_load_fallback",
1300
- {},
1301
- {
1302
- "app.file.candidates": soulPathCandidates()
1303
- },
1304
- "SOUL.md not found; using built-in default personality"
1305
- );
1306
- return DEFAULT_SOUL;
1215
+ await store.append({
1216
+ scope: args,
1217
+ entries: [mcpProviderConnectedEntry(args.provider, sessionId)],
1218
+ ttlMs: args.ttlMs
1219
+ });
1307
1220
  }
1308
- function loadWorld() {
1309
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
1310
- }
1311
- var JUNIOR_PERSONALITY = (() => {
1312
- try {
1313
- return loadSoul();
1314
- } catch (error) {
1315
- logWarn(
1316
- "soul_load_failed",
1317
- {},
1318
- {
1319
- "exception.message": error instanceof Error ? error.message : String(error)
1320
- },
1321
- "Failed to load SOUL.md; using built-in default personality"
1322
- );
1323
- return DEFAULT_SOUL;
1324
- }
1325
- })();
1326
- var JUNIOR_WORLD = (() => {
1327
- try {
1328
- return loadWorld();
1329
- } catch (error) {
1330
- logWarn(
1331
- "world_load_failed",
1332
- {},
1333
- {
1334
- "exception.message": error instanceof Error ? error.message : String(error)
1335
- },
1336
- "Failed to load WORLD.md; omitting world prompt context"
1337
- );
1338
- return null;
1339
- }
1340
- })();
1341
- function workspaceSkillDir(skillName) {
1342
- return sandboxSkillDir(skillName);
1343
- }
1344
- function formatConfigurationValue(value) {
1345
- if (typeof value === "string") {
1346
- return escapeXml(value);
1347
- }
1348
- try {
1349
- return escapeXml(JSON.stringify(value));
1350
- } catch {
1351
- return escapeXml(String(value));
1221
+ async function recordAuthorizationRequested(args) {
1222
+ const store = args.store ?? await defaultStore();
1223
+ const entries = await store.read(args);
1224
+ const sessionId = currentSessionId(entries);
1225
+ if (projectionEntries(entries).some(
1226
+ (entry) => entry.type === "authorization_requested" && entry.authorizationId === args.authorizationId
1227
+ )) {
1228
+ return;
1352
1229
  }
1230
+ await store.append({
1231
+ scope: args,
1232
+ entries: [
1233
+ authorizationRequestedEntry({
1234
+ createdAtMs: Date.now(),
1235
+ kind: args.kind,
1236
+ sessionId,
1237
+ provider: args.provider,
1238
+ requesterId: args.requesterId,
1239
+ authorizationId: args.authorizationId,
1240
+ delivery: args.delivery
1241
+ })
1242
+ ],
1243
+ ttlMs: args.ttlMs
1244
+ });
1353
1245
  }
1354
- function renderRequesterBlock(fields) {
1355
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
1356
- if (lines.length === 0) {
1357
- return null;
1246
+ async function recordAuthorizationCompleted(args) {
1247
+ const store = args.store ?? await defaultStore();
1248
+ const entries = await store.read(args);
1249
+ const sessionId = currentSessionId(entries);
1250
+ if (projectionEntries(entries).some(
1251
+ (entry) => entry.type === "authorization_completed" && entry.authorizationId === args.authorizationId
1252
+ )) {
1253
+ return;
1358
1254
  }
1359
- return ["<requester>", ...lines, "</requester>"];
1255
+ await store.append({
1256
+ scope: args,
1257
+ entries: [
1258
+ authorizationCompletedEntry({
1259
+ createdAtMs: Date.now(),
1260
+ kind: args.kind,
1261
+ sessionId,
1262
+ provider: args.provider,
1263
+ requesterId: args.requesterId,
1264
+ authorizationId: args.authorizationId
1265
+ })
1266
+ ],
1267
+ ttlMs: args.ttlMs
1268
+ });
1360
1269
  }
1361
- function renderTag(tag, lines) {
1362
- return [`<${tag}>`, ...lines, `</${tag}>`];
1270
+ async function commitMessages(args) {
1271
+ const store = args.store ?? await defaultStore();
1272
+ const entries = await store.read(args);
1273
+ const existingProjection = project(entries);
1274
+ const currentId = currentSessionId(entries);
1275
+ const nextEntries = commitEntries(
1276
+ existingProjection.messages,
1277
+ args.messages,
1278
+ currentId,
1279
+ entries,
1280
+ existingProjection.requester,
1281
+ args.requester
1282
+ );
1283
+ await store.append({
1284
+ scope: args,
1285
+ entries: nextEntries,
1286
+ ttlMs: args.ttlMs
1287
+ });
1288
+ return {
1289
+ sessionId: nextEntries.find((entry) => entry.type === "projection_reset")?.sessionId ?? currentId
1290
+ };
1363
1291
  }
1364
- function renderTagBlock(tag, content) {
1365
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
1292
+
1293
+ // src/chat/xml.ts
1294
+ function escapeXml(value) {
1295
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1366
1296
  }
1367
- function formatSkillEntry(skill) {
1368
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
1369
- const lines = [];
1370
- lines.push(" <skill>");
1371
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
1372
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
1373
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
1374
- lines.push(" </skill>");
1375
- return lines;
1297
+
1298
+ // src/chat/prompt.ts
1299
+ import fs from "fs";
1300
+ import path from "path";
1301
+
1302
+ // src/chat/interruption-marker.ts
1303
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
1304
+ function getInterruptionMarker() {
1305
+ return INTERRUPTED_MARKER;
1376
1306
  }
1377
- function formatAvailableSkillsForPrompt(skills, invocation) {
1378
- const autoSelectable = skills.filter(
1379
- (s) => s.disableModelInvocation !== true
1380
- );
1381
- const invokedExplicitOnly = invocation ? skills.filter(
1382
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
1383
- ) : [];
1384
- const sections = [];
1385
- if (autoSelectable.length > 0) {
1386
- const available = [
1387
- "<available-skills>",
1388
- "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
1389
- ];
1390
- for (const skill of autoSelectable) {
1391
- available.push(...formatSkillEntry(skill));
1392
- }
1393
- available.push("</available-skills>");
1394
- sections.push(available.join("\n"));
1307
+
1308
+ // src/chat/slack/status-format.ts
1309
+ var SLACK_STATUS_MAX_LENGTH = 50;
1310
+ function truncateStatusText(text) {
1311
+ const trimmed = text.trim();
1312
+ if (!trimmed) {
1313
+ return "";
1395
1314
  }
1396
- if (invokedExplicitOnly.length > 0) {
1397
- const userCallable = [
1398
- "<user-callable-skills>",
1399
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
1400
- ];
1401
- for (const skill of invokedExplicitOnly) {
1402
- userCallable.push(...formatSkillEntry(skill));
1403
- }
1404
- userCallable.push("</user-callable-skills>");
1405
- sections.push(userCallable.join("\n"));
1315
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
1316
+ return trimmed;
1406
1317
  }
1407
- return sections.length > 0 ? sections.join("\n") : null;
1318
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
1408
1319
  }
1409
- function formatActiveMcpCatalogsForPrompt(catalogs) {
1410
- if (catalogs.length === 0) {
1411
- return null;
1412
- }
1413
- const lines = [
1414
- "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
1415
- ];
1416
- for (const catalog of catalogs) {
1417
- lines.push(" <catalog>");
1418
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
1419
- lines.push(
1420
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
1421
- );
1422
- lines.push(" </catalog>");
1320
+
1321
+ // src/chat/slack/mrkdwn.ts
1322
+ function readInlineCodeSpan(line, start) {
1323
+ if (line[start] !== "`") {
1324
+ return void 0;
1423
1325
  }
1424
- return lines.join("\n");
1425
- }
1426
- function formatToolGuidanceForPrompt(tools) {
1427
- const guidedTools = tools.filter(
1428
- (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
1429
- );
1430
- if (guidedTools.length === 0) {
1431
- return null;
1326
+ let n = 1;
1327
+ while (line[start + n] === "`") {
1328
+ n++;
1432
1329
  }
1433
- const lines = [];
1434
- for (const tool of guidedTools) {
1435
- lines.push(` <tool name="${escapeXml(tool.name)}">`);
1436
- if (tool.promptSnippet?.trim()) {
1437
- lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
1330
+ const marker = "`".repeat(n);
1331
+ let search = start + n;
1332
+ while (search < line.length) {
1333
+ const close = line.indexOf(marker, search);
1334
+ if (close === -1) {
1335
+ return void 0;
1438
1336
  }
1439
- if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1440
- for (const guideline of tool.promptGuidelines) {
1441
- lines.push(` - ${escapeXml(guideline)}`);
1442
- }
1337
+ const after = close + n;
1338
+ if (line[after] !== "`") {
1339
+ return { text: line.slice(start, after), end: after };
1443
1340
  }
1444
- lines.push(" </tool>");
1341
+ search = after + 1;
1445
1342
  }
1446
- return lines.join("\n");
1343
+ return void 0;
1447
1344
  }
1448
- function formatReferenceFilesLines() {
1449
- const files = listReferenceFiles();
1450
- if (files.length === 0) {
1451
- return null;
1345
+ function readExistingSlackAngleToken(line, start) {
1346
+ if (line[start] !== "<") {
1347
+ return void 0;
1452
1348
  }
1453
- return files.map((filePath) => {
1454
- const name = path.basename(filePath);
1455
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1456
- });
1457
- }
1458
- function formatArtifactsLines(artifactState) {
1459
- if (!artifactState) return null;
1460
- const lines = [];
1461
- if (artifactState.lastCanvasId) {
1462
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
1349
+ const close = line.indexOf(">", start + 1);
1350
+ if (close === -1) {
1351
+ return void 0;
1463
1352
  }
1464
- if (artifactState.lastCanvasUrl) {
1465
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1353
+ const body = line.slice(start + 1, close);
1354
+ if (/^(?:https?:\/\/|@|#|!)/.test(body)) {
1355
+ return { text: line.slice(start, close + 1), end: close + 1 };
1466
1356
  }
1467
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1468
- lines.push("- recent_canvases:");
1469
- for (const canvas of artifactState.recentCanvases) {
1470
- lines.push(` - id: ${escapeXml(canvas.id)}`);
1471
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1472
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1473
- if (canvas.createdAt) {
1474
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1475
- }
1476
- }
1357
+ return void 0;
1358
+ }
1359
+ function readMarkdownLink(line, start) {
1360
+ if (line[start] !== "[") {
1361
+ return void 0;
1477
1362
  }
1478
- if (artifactState.lastListId) {
1479
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1363
+ const labelEnd = line.indexOf("](", start + 1);
1364
+ if (labelEnd === -1) {
1365
+ return void 0;
1480
1366
  }
1481
- if (artifactState.lastListUrl) {
1482
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
1367
+ const destStart = labelEnd + 2;
1368
+ if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) {
1369
+ return void 0;
1483
1370
  }
1484
- return lines.length > 0 ? lines : null;
1485
- }
1486
- function formatConfigurationLines(configuration) {
1487
- const keys = Object.keys(configuration ?? {}).sort(
1488
- (a, b) => a.localeCompare(b)
1489
- );
1490
- if (keys.length === 0) return null;
1491
- return keys.map(
1492
- (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
1493
- );
1494
- }
1495
- var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
1496
- var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1497
- var TOOL_POLICY_RULES = [
1498
- "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
1499
- "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
1500
- "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1501
- "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
1502
- "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
1503
- `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
1504
- "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
1505
- "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1506
- "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1507
- ];
1508
- var TOOL_CALL_STYLE_RULES = [
1509
- "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1510
- "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1511
- "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
1512
- "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1513
- ];
1514
- var SKILL_POLICY_RULES = [
1515
- "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1516
- "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1517
- ];
1518
- var EXECUTION_CONTRACT_RULES = [
1519
- "- Actionable request: act in this turn.",
1520
- "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
1521
- "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
1522
- "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1523
- "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1524
- "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
1525
- ];
1526
- var CONVERSATION_RULES = [
1527
- "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1528
- "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1529
- "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1530
- ];
1531
- var SLACK_ACTION_RULES = [
1532
- "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1533
- "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1534
- "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1535
- "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
1536
- "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
1537
- "- Do not use reactions as progress indicators."
1538
- ];
1539
- var SAFETY_RULES = [
1540
- "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1541
- "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1542
- "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
1543
- ];
1544
- var FAILURE_RULES = [
1545
- "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1546
- "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
1547
- "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
1548
- ];
1549
- function renderRuleSection(tag, lines) {
1550
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
1551
- }
1552
- function buildBehaviorSection() {
1553
- return [
1554
- renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1555
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1556
- renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1557
- renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
1558
- renderRuleSection("conversation", CONVERSATION_RULES),
1559
- renderRuleSection("slack-actions", SLACK_ACTION_RULES),
1560
- renderRuleSection("safety", SAFETY_RULES),
1561
- renderRuleSection("failure-handling", FAILURE_RULES)
1562
- ].join("\n\n");
1563
- }
1564
- function buildOutputSection() {
1565
- const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
1566
- return [
1567
- openTag,
1568
- "- Start with the answer or result, not internal process narration.",
1569
- "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
1570
- "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
1571
- "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
1572
- "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
1573
- "</output>"
1574
- ].join("\n");
1575
- }
1576
- function buildIdentitySection() {
1577
- return [
1578
- "# Identity",
1579
- `Your Slack username is \`${botConfig.userName}\`.`
1580
- ].join("\n");
1581
- }
1582
- function buildPersonalitySection() {
1583
- return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
1584
- }
1585
- function buildWorldSection() {
1586
- if (!JUNIOR_WORLD) {
1587
- return null;
1371
+ const closeParens = line.indexOf(")", destStart);
1372
+ if (closeParens === -1) {
1373
+ return void 0;
1588
1374
  }
1589
- return ["# World", JUNIOR_WORLD.trim()].join("\n");
1375
+ return { text: line.slice(start, closeParens + 1), end: closeParens + 1 };
1590
1376
  }
1591
- function buildRuntimeSection(params) {
1592
- const lines = [
1593
- params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
1594
- params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
1595
- params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
1596
- ].filter(Boolean);
1597
- if (lines.length === 0) {
1598
- return null;
1377
+ function hasUnmatchedClosingParen(text) {
1378
+ let balance = 0;
1379
+ for (const ch of text) {
1380
+ if (ch === "(") balance++;
1381
+ else if (ch === ")") balance--;
1599
1382
  }
1600
- return renderTagBlock("runtime", lines.join("\n"));
1383
+ return balance < 0;
1601
1384
  }
1602
- function buildContextSection(params) {
1603
- const blocks = [];
1604
- const referenceLines = formatReferenceFilesLines();
1605
- if (referenceLines) {
1606
- blocks.push(
1607
- renderTag("reference-files", [
1608
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
1609
- ...referenceLines
1610
- ])
1611
- );
1612
- }
1613
- const requesterLines = renderRequesterBlock({
1614
- full_name: params.requester?.fullName,
1615
- user_name: params.requester?.userName,
1616
- user_id: params.requester?.userId
1617
- });
1618
- if (requesterLines) {
1619
- blocks.push(requesterLines);
1385
+ function readBareUrl(line, start) {
1386
+ let end = start;
1387
+ while (end < line.length) {
1388
+ const ch = line[end];
1389
+ if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === '"' || ch === "`" || ch === "|" || ch === "*") {
1390
+ break;
1391
+ }
1392
+ end++;
1620
1393
  }
1621
- const artifactLines = formatArtifactsLines(params.artifactState);
1622
- if (artifactLines) {
1623
- blocks.push(renderTag("artifacts", artifactLines));
1394
+ if (end === start) {
1395
+ return void 0;
1624
1396
  }
1625
- const configLines = formatConfigurationLines(params.configuration);
1626
- if (configLines) {
1627
- blocks.push(
1628
- renderTag("configuration", [
1629
- "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
1630
- ...configLines
1631
- ])
1632
- );
1397
+ let raw = line.slice(start, end);
1398
+ let suffix = "";
1399
+ const peel = () => {
1400
+ suffix = raw.slice(-1) + suffix;
1401
+ raw = raw.slice(0, -1);
1402
+ };
1403
+ const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
1404
+ while (raw.length > 0 && shouldPeel()) {
1405
+ peel();
1633
1406
  }
1634
- if (params.invocation) {
1635
- blocks.push(
1636
- renderTag("explicit-skill-trigger", [
1637
- "Treat this skill as selected. Load it unless the tool says it is unavailable.",
1638
- `/${escapeXml(params.invocation.skillName)}`
1639
- ])
1640
- );
1407
+ if (!/^https?:\/\/.+/.test(raw)) {
1408
+ return void 0;
1641
1409
  }
1642
- const body = blocks.map((block) => block.join("\n")).join("\n\n");
1643
- if (!body) {
1644
- return null;
1410
+ return { url: raw, suffix, end };
1411
+ }
1412
+ function wrapBareUrlsOnLine(line) {
1413
+ let result = "";
1414
+ let i = 0;
1415
+ while (i < line.length) {
1416
+ const codeSpan = readInlineCodeSpan(line, i);
1417
+ if (codeSpan) {
1418
+ result += codeSpan.text;
1419
+ i = codeSpan.end;
1420
+ continue;
1421
+ }
1422
+ const angleToken = readExistingSlackAngleToken(line, i);
1423
+ if (angleToken) {
1424
+ result += angleToken.text;
1425
+ i = angleToken.end;
1426
+ continue;
1427
+ }
1428
+ const mdLink = readMarkdownLink(line, i);
1429
+ if (mdLink) {
1430
+ result += mdLink.text;
1431
+ i = mdLink.end;
1432
+ continue;
1433
+ }
1434
+ if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
1435
+ const parsed = readBareUrl(line, i);
1436
+ if (parsed) {
1437
+ result += `<${parsed.url}>${parsed.suffix}`;
1438
+ i = parsed.end;
1439
+ continue;
1440
+ }
1441
+ }
1442
+ result += line[i];
1443
+ i++;
1645
1444
  }
1646
- return renderTagBlock("context", body);
1445
+ return result;
1647
1446
  }
1648
- function buildCapabilitiesSection(params) {
1649
- const blocks = [];
1650
- const availableSkills = formatAvailableSkillsForPrompt(
1651
- params.availableSkills,
1652
- params.invocation
1653
- );
1654
- if (availableSkills) {
1655
- blocks.push(availableSkills);
1447
+ function wrapBareUrls(text) {
1448
+ const lines = text.split("\n");
1449
+ const out = [];
1450
+ let inCodeBlock = false;
1451
+ for (const line of lines) {
1452
+ if (line.trimStart().startsWith("```")) {
1453
+ inCodeBlock = !inCodeBlock;
1454
+ out.push(line);
1455
+ continue;
1456
+ }
1457
+ out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
1656
1458
  }
1657
- const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1658
- params.activeMcpCatalogs
1659
- );
1660
- if (activeCatalogs) {
1661
- blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
1459
+ return out.join("\n");
1460
+ }
1461
+ function ensureBlockSpacing(text) {
1462
+ const codeBlockPattern = /^```/;
1463
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
1464
+ const lines = text.split("\n");
1465
+ const result = [];
1466
+ let inCodeBlock = false;
1467
+ for (let i = 0; i < lines.length; i++) {
1468
+ const line = lines[i];
1469
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
1470
+ if (isCodeFence) {
1471
+ if (!inCodeBlock) {
1472
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
1473
+ if (prev2 !== void 0 && prev2.trim() !== "") {
1474
+ result.push("");
1475
+ }
1476
+ }
1477
+ inCodeBlock = !inCodeBlock;
1478
+ result.push(line);
1479
+ continue;
1480
+ }
1481
+ if (inCodeBlock) {
1482
+ result.push(line);
1483
+ continue;
1484
+ }
1485
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
1486
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
1487
+ result.push("");
1488
+ }
1489
+ result.push(line);
1662
1490
  }
1663
- const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
1664
- if (toolGuidance) {
1665
- blocks.push(renderTagBlock("tool-guidance", toolGuidance));
1491
+ return result.join("\n");
1492
+ }
1493
+ function normalizeSlackReplyMarkdown(text) {
1494
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1495
+ normalized = wrapBareUrls(normalized);
1496
+ normalized = ensureBlockSpacing(normalized);
1497
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
1498
+ }
1499
+ function normalizeSlackStatusText(text) {
1500
+ const trimmed = text.trim();
1501
+ if (!trimmed) {
1502
+ return "";
1666
1503
  }
1667
- if (blocks.length === 0) {
1668
- return null;
1504
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
1505
+ }
1506
+
1507
+ // src/chat/slack/output.ts
1508
+ var MAX_INLINE_CHARS = 2200;
1509
+ var MAX_INLINE_LINES = 45;
1510
+ var CONTINUED_MARKER = "\n\n[Continued below]";
1511
+ function countSlackLines(text) {
1512
+ if (!text) {
1513
+ return 0;
1669
1514
  }
1670
- return blocks.join("\n\n");
1515
+ return text.split("\n").length;
1671
1516
  }
1672
- var STATIC_SYSTEM_PROMPT = [
1673
- HEADER,
1674
- buildIdentitySection(),
1675
- buildPersonalitySection(),
1676
- buildWorldSection(),
1677
- buildBehaviorSection(),
1678
- buildOutputSection()
1679
- ].filter((section) => Boolean(section)).join("\n\n");
1680
- function buildSystemPrompt() {
1681
- return STATIC_SYSTEM_PROMPT;
1517
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1518
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
1682
1519
  }
1683
- function buildTurnContextPrompt(params) {
1684
- const includeSessionContext = params.includeSessionContext ?? true;
1685
- if (!includeSessionContext) {
1686
- return null;
1520
+ function findSplitIndex(text, maxChars) {
1521
+ if (text.length <= maxChars) {
1522
+ return text.length;
1687
1523
  }
1688
- const runtimeSections = [
1689
- buildCapabilitiesSection({
1690
- availableSkills: params.availableSkills,
1691
- activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1692
- invocation: params.invocation,
1693
- toolGuidance: params.toolGuidance ?? []
1694
- }),
1695
- buildContextSection({
1696
- requester: params.requester,
1697
- artifactState: params.artifactState,
1698
- configuration: params.configuration,
1699
- invocation: params.invocation
1700
- }),
1701
- buildRuntimeSection(params.runtime ?? {})
1702
- ].filter((section) => Boolean(section));
1703
- if (runtimeSections.length === 0) {
1524
+ const bounded = text.slice(0, maxChars);
1525
+ const newlineIndex = bounded.lastIndexOf("\n");
1526
+ if (newlineIndex > 0) {
1527
+ return newlineIndex;
1528
+ }
1529
+ const spaceIndex = bounded.lastIndexOf(" ");
1530
+ if (spaceIndex > 0) {
1531
+ return spaceIndex;
1532
+ }
1533
+ return maxChars;
1534
+ }
1535
+ function splitByLineBudget(text, maxLines) {
1536
+ if (maxLines <= 0) {
1537
+ return "";
1538
+ }
1539
+ const lines = text.split("\n");
1540
+ if (lines.length <= maxLines) {
1541
+ return text;
1542
+ }
1543
+ return lines.slice(0, maxLines).join("\n");
1544
+ }
1545
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1546
+ return {
1547
+ maxChars: Math.max(1, maxChars - suffix.length),
1548
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1549
+ };
1550
+ }
1551
+ function forceSplitBudget(text, budget) {
1552
+ const lineCount = countSlackLines(text);
1553
+ return {
1554
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1555
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1556
+ };
1557
+ }
1558
+ function getFenceContinuation(text) {
1559
+ let open;
1560
+ for (const line of text.split("\n")) {
1561
+ const trimmed = line.trimStart();
1562
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1563
+ if (!openerMatch) {
1564
+ continue;
1565
+ }
1566
+ if (!open) {
1567
+ open = {
1568
+ fence: openerMatch[1],
1569
+ openerLine: trimmed
1570
+ };
1571
+ continue;
1572
+ }
1573
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1574
+ open = void 0;
1575
+ }
1576
+ }
1577
+ if (!open) {
1704
1578
  return null;
1705
1579
  }
1706
- const sections = [
1707
- `<${TURN_CONTEXT_TAG}>`,
1708
- TURN_CONTEXT_HEADER,
1709
- "The current user instruction appears after this block in the same message.",
1710
- ...runtimeSections,
1711
- `</${TURN_CONTEXT_TAG}>`
1712
- ].filter((section) => Boolean(section));
1713
- return sections.join("\n\n");
1580
+ return {
1581
+ closeSuffix: text.endsWith("\n") ? open.fence : `
1582
+ ${open.fence}`,
1583
+ reopenPrefix: `${open.openerLine}
1584
+ `
1585
+ };
1714
1586
  }
1715
-
1716
- // src/chat/state/session-log.ts
1717
- import { isDeepStrictEqual } from "util";
1718
- import { z } from "zod";
1719
- var AGENT_SESSION_LOG_PREFIX = "junior:agent-session-log";
1720
- var AGENT_SESSION_LOG_SCHEMA_VERSION = 1;
1721
- var INITIAL_SESSION_ID = "session_0";
1722
- var SESSION_ID_PREFIX = "session_";
1723
- var piMessageSchema = z.object({
1724
- role: z.string()
1725
- }).passthrough().transform((value) => value);
1726
- var piMessageEntrySchema = z.object({
1727
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1728
- type: z.literal("pi_message"),
1729
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1730
- message: piMessageSchema,
1731
- requester: storedSlackRequesterSchema.optional()
1732
- });
1733
- var projectionResetEntrySchema = z.object({
1734
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1735
- type: z.literal("projection_reset"),
1736
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1737
- messages: z.array(piMessageSchema),
1738
- requester: storedSlackRequesterSchema.optional()
1739
- });
1740
- var requesterRecordedEntrySchema = z.object({
1741
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1742
- type: z.literal("requester_recorded"),
1743
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1744
- requester: storedSlackRequesterSchema
1745
- });
1746
- var mcpProviderConnectedEntrySchema = z.object({
1747
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1748
- type: z.literal("mcp_provider_connected"),
1749
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1750
- provider: z.string().min(1)
1751
- });
1752
- var authorizationKindSchema = z.union([
1753
- z.literal("plugin"),
1754
- z.literal("mcp")
1755
- ]);
1756
- var authorizationRequestedEntrySchema = z.object({
1757
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1758
- type: z.literal("authorization_requested"),
1759
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1760
- createdAtMs: z.number().int().nonnegative(),
1761
- kind: authorizationKindSchema,
1762
- provider: z.string().min(1),
1763
- requesterId: z.string().min(1),
1764
- authorizationId: z.string().min(1),
1765
- delivery: z.union([
1766
- z.literal("private_link_sent"),
1767
- z.literal("private_link_reused")
1768
- ])
1769
- });
1770
- var authorizationCompletedEntrySchema = z.object({
1771
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1772
- type: z.literal("authorization_completed"),
1773
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1774
- createdAtMs: z.number().int().nonnegative(),
1775
- kind: authorizationKindSchema,
1776
- provider: z.string().min(1),
1777
- requesterId: z.string().min(1),
1778
- authorizationId: z.string().min(1)
1779
- });
1780
- var sessionLogEntrySchema = z.discriminatedUnion("type", [
1781
- piMessageEntrySchema,
1782
- projectionResetEntrySchema,
1783
- requesterRecordedEntrySchema,
1784
- mcpProviderConnectedEntrySchema,
1785
- authorizationRequestedEntrySchema,
1786
- authorizationCompletedEntrySchema
1787
- ]);
1788
- function key(scope) {
1789
- const prefix = getChatConfig().state.keyPrefix;
1790
- return [
1791
- ...prefix ? [prefix] : [],
1792
- AGENT_SESSION_LOG_PREFIX,
1793
- scope.conversationId
1794
- ].join(":");
1587
+ function appendSlackSuffix(text, marker) {
1588
+ const carryover = getFenceContinuation(text);
1589
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1795
1590
  }
1796
- function rawKey(scope) {
1797
- return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
1591
+ function stripTrailingContinuationMarker(text) {
1592
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1798
1593
  }
1799
- function normalizeMessageCount(value) {
1800
- return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
1594
+ function takeSlackContinuationChunk(text, budget) {
1595
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1596
+ if (!rest) {
1597
+ ({ prefix, rest } = takeSlackInlinePrefix(
1598
+ text,
1599
+ forceSplitBudget(text, budget)
1600
+ ));
1601
+ }
1602
+ let carryover = rest ? getFenceContinuation(prefix) : null;
1603
+ if (!carryover) {
1604
+ return { prefix, rest };
1605
+ }
1606
+ const carryoverBudget = reserveInlineBudgetForSuffix(
1607
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
1608
+ );
1609
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1610
+ if (!rest) {
1611
+ ({ prefix, rest } = takeSlackInlinePrefix(
1612
+ text,
1613
+ forceSplitBudget(text, carryoverBudget)
1614
+ ));
1615
+ }
1616
+ carryover = rest ? getFenceContinuation(prefix) : null;
1617
+ if (!carryover) {
1618
+ return { prefix, rest };
1619
+ }
1620
+ return {
1621
+ prefix,
1622
+ rest: `${carryover.reopenPrefix}${rest}`
1623
+ };
1801
1624
  }
1802
- function countMatchingPrefix(left, right) {
1803
- const limit = Math.min(left.length, right.length);
1804
- for (let index = 0; index < limit; index += 1) {
1805
- if (!isDeepStrictEqual(left[index], right[index])) {
1806
- return index;
1625
+ function takeSlackContinuationPrefix(text, options) {
1626
+ const budget = {
1627
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1628
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1629
+ };
1630
+ const { prefix, rest } = (() => {
1631
+ if (options?.forceSplit) {
1632
+ return takeSlackContinuationChunk(text, budget);
1807
1633
  }
1634
+ const initial = takeSlackInlinePrefix(text, budget);
1635
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1636
+ })();
1637
+ return {
1638
+ prefix,
1639
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1640
+ rest
1641
+ };
1642
+ }
1643
+ function takeSlackInlinePrefix(text, options) {
1644
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1645
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1646
+ const normalized = text.replace(/\r\n?/g, "\n");
1647
+ if (!normalized) {
1648
+ return { prefix: "", rest: "" };
1808
1649
  }
1809
- return limit;
1650
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1651
+ return { prefix: normalized, rest: "" };
1652
+ }
1653
+ const lineBounded = splitByLineBudget(normalized, maxLines);
1654
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
1655
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1656
+ if (prefix) {
1657
+ return {
1658
+ prefix,
1659
+ rest: normalized.slice(prefix.length).trimStart()
1660
+ };
1661
+ }
1662
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1663
+ return {
1664
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1665
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1666
+ };
1810
1667
  }
1811
- function entrySessionId(entry) {
1812
- return entry.sessionId ?? INITIAL_SESSION_ID;
1668
+ function splitSlackReplyText(text, options) {
1669
+ const normalized = normalizeSlackReplyMarkdown(text);
1670
+ if (!normalized) {
1671
+ return [];
1672
+ }
1673
+ const chunks = [];
1674
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1675
+ let remaining = normalized;
1676
+ while (remaining) {
1677
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1678
+ if (fitsFinalChunk) {
1679
+ chunks.push(
1680
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1681
+ );
1682
+ break;
1683
+ }
1684
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1685
+ ...continuationBudget,
1686
+ forceSplit: true
1687
+ });
1688
+ chunks.push(renderedPrefix);
1689
+ remaining = rest;
1690
+ }
1691
+ if (chunks.length === 2) {
1692
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1693
+ }
1694
+ return chunks;
1813
1695
  }
1814
- function latestProjectionResetIndex(entries) {
1815
- for (let index = entries.length - 1; index >= 0; index -= 1) {
1816
- if (entries[index]?.type === "projection_reset") {
1817
- return index;
1696
+ function getSlackContinuationBudget() {
1697
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1698
+ }
1699
+ function buildSlackOutputMessage(text, files) {
1700
+ const normalized = normalizeSlackReplyMarkdown(text);
1701
+ const fileCount = files?.length ?? 0;
1702
+ if (!normalized) {
1703
+ if (fileCount > 0) {
1704
+ return {
1705
+ raw: "",
1706
+ files
1707
+ };
1708
+ }
1709
+ throw new Error(
1710
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1711
+ );
1712
+ }
1713
+ return {
1714
+ markdown: normalized,
1715
+ files
1716
+ };
1717
+ }
1718
+ var slackOutputPolicy = {
1719
+ maxInlineChars: MAX_INLINE_CHARS,
1720
+ maxInlineLines: MAX_INLINE_LINES
1721
+ };
1722
+
1723
+ // src/chat/prompt.ts
1724
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1725
+ function getLoggedMarkdownFiles() {
1726
+ const globalState = globalThis;
1727
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
1728
+ return globalState.__juniorLoggedMarkdownFiles;
1729
+ }
1730
+ function loadOptionalMarkdownFile(candidates, fileName) {
1731
+ for (const resolved of candidates) {
1732
+ try {
1733
+ const raw = fs.readFileSync(resolved, "utf8").trim();
1734
+ if (raw.length > 0) {
1735
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
1736
+ const logKey = `${fileName}:${resolved}`;
1737
+ if (!loggedMarkdownFiles.has(logKey)) {
1738
+ loggedMarkdownFiles.add(logKey);
1739
+ logInfo(
1740
+ `${fileName.toLowerCase()}_loaded`,
1741
+ {},
1742
+ {
1743
+ "file.path": resolved
1744
+ },
1745
+ `Loaded ${fileName}`
1746
+ );
1747
+ }
1748
+ return raw;
1749
+ }
1750
+ } catch {
1751
+ continue;
1818
1752
  }
1819
1753
  }
1820
- return -1;
1754
+ return null;
1821
1755
  }
1822
- function currentSessionId(entries) {
1823
- const resetIndex = latestProjectionResetIndex(entries);
1824
- if (resetIndex < 0) {
1825
- return INITIAL_SESSION_ID;
1756
+ function loadSoul() {
1757
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1758
+ if (soul) {
1759
+ return soul;
1826
1760
  }
1827
- return entrySessionId(entries[resetIndex]);
1761
+ logWarn(
1762
+ "soul_load_fallback",
1763
+ {},
1764
+ {
1765
+ "app.file.candidates": soulPathCandidates()
1766
+ },
1767
+ "SOUL.md not found; using built-in default personality"
1768
+ );
1769
+ return DEFAULT_SOUL;
1828
1770
  }
1829
- function nextSessionId(entries) {
1830
- const resetCount = entries.filter(
1831
- (entry) => entry.type === "projection_reset"
1832
- ).length;
1833
- return `${SESSION_ID_PREFIX}${resetCount + 1}`;
1771
+ function loadWorld() {
1772
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
1834
1773
  }
1835
- function projectionEntries(entries, sessionId) {
1836
- if (sessionId) {
1837
- const sessionEntries = [];
1838
- let started = false;
1839
- for (const entry of entries) {
1840
- const entryId = entrySessionId(entry);
1841
- if (!started) {
1842
- if (entryId !== sessionId) {
1843
- continue;
1844
- }
1845
- started = true;
1846
- } else if (entry.type === "projection_reset" && entryId !== sessionId) {
1847
- break;
1848
- }
1849
- if (entryId === sessionId) {
1850
- sessionEntries.push(entry);
1851
- }
1852
- }
1853
- return sessionEntries;
1774
+ var JUNIOR_PERSONALITY = (() => {
1775
+ try {
1776
+ return loadSoul();
1777
+ } catch (error) {
1778
+ logWarn(
1779
+ "soul_load_failed",
1780
+ {},
1781
+ {
1782
+ "exception.message": error instanceof Error ? error.message : String(error)
1783
+ },
1784
+ "Failed to load SOUL.md; using built-in default personality"
1785
+ );
1786
+ return DEFAULT_SOUL;
1854
1787
  }
1855
- const resetIndex = latestProjectionResetIndex(entries);
1856
- const startIndex = resetIndex < 0 ? 0 : resetIndex;
1857
- const currentId = resetIndex < 0 ? INITIAL_SESSION_ID : entrySessionId(entries[resetIndex]);
1858
- return entries.slice(startIndex).filter((entry) => entrySessionId(entry) === currentId);
1859
- }
1860
- function findLastIndex(values, predicate) {
1861
- for (let index = values.length - 1; index >= 0; index -= 1) {
1862
- if (predicate(values[index])) return index;
1788
+ })();
1789
+ var JUNIOR_WORLD = (() => {
1790
+ try {
1791
+ return loadWorld();
1792
+ } catch (error) {
1793
+ logWarn(
1794
+ "world_load_failed",
1795
+ {},
1796
+ {
1797
+ "exception.message": error instanceof Error ? error.message : String(error)
1798
+ },
1799
+ "Failed to load WORLD.md; omitting world prompt context"
1800
+ );
1801
+ return null;
1863
1802
  }
1864
- return -1;
1865
- }
1866
- function piEntry(message, sessionId, requester) {
1867
- return {
1868
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1869
- type: "pi_message",
1870
- sessionId,
1871
- message,
1872
- ...requester ? { requester } : {}
1873
- };
1803
+ })();
1804
+ function workspaceSkillDir(skillName) {
1805
+ return sandboxSkillDir(skillName);
1874
1806
  }
1875
- function resetEntry(messages, sessionId, requester) {
1876
- return {
1877
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1878
- type: "projection_reset",
1879
- sessionId,
1880
- messages,
1881
- ...requester ? { requester } : {}
1882
- };
1807
+ function formatConfigurationValue(value) {
1808
+ if (typeof value === "string") {
1809
+ return escapeXml(value);
1810
+ }
1811
+ try {
1812
+ return escapeXml(JSON.stringify(value));
1813
+ } catch {
1814
+ return escapeXml(String(value));
1815
+ }
1883
1816
  }
1884
- function requesterRecordedEntry(requester, sessionId) {
1885
- return {
1886
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1887
- type: "requester_recorded",
1888
- sessionId,
1889
- requester
1890
- };
1817
+ function renderRequesterBlock(fields) {
1818
+ const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
1819
+ if (lines.length === 0) {
1820
+ return null;
1821
+ }
1822
+ return ["<requester>", ...lines, "</requester>"];
1891
1823
  }
1892
- function mcpProviderConnectedEntry(provider, sessionId) {
1893
- return {
1894
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1895
- type: "mcp_provider_connected",
1896
- sessionId,
1897
- provider
1898
- };
1824
+ function renderTag(tag, lines) {
1825
+ return [`<${tag}>`, ...lines, `</${tag}>`];
1899
1826
  }
1900
- function authorizationObservationMessage(entry) {
1901
- const label = entry.kind === "mcp" ? "MCP authorization" : "Authorization";
1902
- return {
1903
- role: "user",
1904
- content: [
1905
- {
1906
- type: "text",
1907
- text: `${label} completed for provider "${entry.provider}". Continue the blocked request and retry the provider operation if needed.`
1908
- }
1909
- ],
1910
- timestamp: entry.createdAtMs
1911
- };
1827
+ function renderTagBlock(tag, content) {
1828
+ return [`<${tag}>`, content, `</${tag}>`].join("\n");
1912
1829
  }
1913
- function authorizationRequestedEntry(args) {
1914
- return {
1915
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1916
- type: "authorization_requested",
1917
- sessionId: args.sessionId,
1918
- createdAtMs: args.createdAtMs,
1919
- kind: args.kind,
1920
- provider: args.provider,
1921
- requesterId: args.requesterId,
1922
- authorizationId: args.authorizationId,
1923
- delivery: args.delivery
1924
- };
1830
+ function formatSkillEntry(skill) {
1831
+ const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
1832
+ const lines = [];
1833
+ lines.push(" <skill>");
1834
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
1835
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
1836
+ lines.push(` <location>${escapeXml(skillLocation)}</location>`);
1837
+ lines.push(" </skill>");
1838
+ return lines;
1925
1839
  }
1926
- function authorizationCompletedEntry(args) {
1927
- return {
1928
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1929
- type: "authorization_completed",
1930
- sessionId: args.sessionId,
1931
- createdAtMs: args.createdAtMs,
1932
- kind: args.kind,
1933
- provider: args.provider,
1934
- requesterId: args.requesterId,
1935
- authorizationId: args.authorizationId
1936
- };
1840
+ function formatAvailableSkillsForPrompt(skills, invocation) {
1841
+ const autoSelectable = skills.filter(
1842
+ (s) => s.disableModelInvocation !== true
1843
+ );
1844
+ const invokedExplicitOnly = invocation ? skills.filter(
1845
+ (s) => s.disableModelInvocation === true && s.name === invocation.skillName
1846
+ ) : [];
1847
+ const sections = [];
1848
+ if (autoSelectable.length > 0) {
1849
+ const available = [
1850
+ "<available-skills>",
1851
+ "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
1852
+ ];
1853
+ for (const skill of autoSelectable) {
1854
+ available.push(...formatSkillEntry(skill));
1855
+ }
1856
+ available.push("</available-skills>");
1857
+ sections.push(available.join("\n"));
1858
+ }
1859
+ if (invokedExplicitOnly.length > 0) {
1860
+ const userCallable = [
1861
+ "<user-callable-skills>",
1862
+ "The user's current message explicitly references this skill by name. Load it when relevant to the request."
1863
+ ];
1864
+ for (const skill of invokedExplicitOnly) {
1865
+ userCallable.push(...formatSkillEntry(skill));
1866
+ }
1867
+ userCallable.push("</user-callable-skills>");
1868
+ sections.push(userCallable.join("\n"));
1869
+ }
1870
+ return sections.length > 0 ? sections.join("\n") : null;
1937
1871
  }
1938
- function decode(value) {
1939
- if (typeof value === "string") {
1940
- return decode(JSON.parse(value));
1872
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
1873
+ if (catalogs.length === 0) {
1874
+ return null;
1941
1875
  }
1942
- const parsed = sessionLogEntrySchema.safeParse(value);
1943
- if (parsed.success) {
1944
- return parsed.data;
1876
+ const lines = [
1877
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
1878
+ ];
1879
+ for (const catalog of catalogs) {
1880
+ lines.push(" <catalog>");
1881
+ lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
1882
+ lines.push(
1883
+ ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
1884
+ );
1885
+ lines.push(" </catalog>");
1945
1886
  }
1946
- return piEntry(piMessageSchema.parse(value), INITIAL_SESSION_ID);
1887
+ return lines.join("\n");
1947
1888
  }
1948
- function project(entries, sessionId) {
1949
- let messages = [];
1950
- let requester;
1951
- for (const entry of projectionEntries(entries, sessionId)) {
1952
- if (entry.type === "pi_message") {
1953
- messages.push(entry.message);
1954
- if (entry.message.role === "user" && entry.requester) {
1955
- requester = entry.requester;
1956
- }
1957
- continue;
1958
- }
1959
- if (entry.type === "requester_recorded") {
1960
- requester = entry.requester;
1961
- continue;
1962
- }
1963
- if (entry.type === "authorization_completed") {
1964
- messages.push(authorizationObservationMessage(entry));
1965
- continue;
1966
- }
1967
- if (entry.type === "mcp_provider_connected" || entry.type === "authorization_requested") {
1968
- continue;
1889
+ function formatToolGuidanceForPrompt(tools) {
1890
+ const guidedTools = tools.filter(
1891
+ (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
1892
+ );
1893
+ if (guidedTools.length === 0) {
1894
+ return null;
1895
+ }
1896
+ const lines = [];
1897
+ for (const tool of guidedTools) {
1898
+ lines.push(` <tool name="${escapeXml(tool.name)}">`);
1899
+ if (tool.promptSnippet?.trim()) {
1900
+ lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
1969
1901
  }
1970
- messages = [...entry.messages];
1971
- if (entry.requester) {
1972
- requester = entry.requester;
1902
+ if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1903
+ for (const guideline of tool.promptGuidelines) {
1904
+ lines.push(` - ${escapeXml(guideline)}`);
1905
+ }
1973
1906
  }
1907
+ lines.push(" </tool>");
1974
1908
  }
1975
- return { messages, requester };
1909
+ return lines.join("\n");
1976
1910
  }
1977
- function projectMessages(entries, sessionId) {
1978
- return project(entries, sessionId).messages;
1911
+ function formatReferenceFilesLines() {
1912
+ const files = listReferenceFiles();
1913
+ if (files.length === 0) {
1914
+ return null;
1915
+ }
1916
+ return files.map((filePath) => {
1917
+ const name = path.basename(filePath);
1918
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1919
+ });
1979
1920
  }
1980
- function connectedMcpProviders(entries, sessionId) {
1981
- const providers = /* @__PURE__ */ new Set();
1982
- for (const entry of projectionEntries(entries, sessionId)) {
1983
- if (entry.type === "mcp_provider_connected") {
1984
- providers.add(entry.provider);
1921
+ function formatArtifactsLines(artifactState) {
1922
+ if (!artifactState) return null;
1923
+ const lines = [];
1924
+ if (artifactState.lastCanvasId) {
1925
+ lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
1926
+ }
1927
+ if (artifactState.lastCanvasUrl) {
1928
+ lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1929
+ }
1930
+ if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1931
+ lines.push("- recent_canvases:");
1932
+ for (const canvas of artifactState.recentCanvases) {
1933
+ lines.push(` - id: ${escapeXml(canvas.id)}`);
1934
+ if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1935
+ if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1936
+ if (canvas.createdAt) {
1937
+ lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1938
+ }
1985
1939
  }
1986
1940
  }
1987
- return [...providers].sort((left, right) => left.localeCompare(right));
1941
+ if (artifactState.lastListId) {
1942
+ lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1943
+ }
1944
+ if (artifactState.lastListUrl) {
1945
+ lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
1946
+ }
1947
+ return lines.length > 0 ? lines : null;
1988
1948
  }
1989
- function commitEntries(existingMessages, nextMessages, sessionId, entries, existingRequester, requester) {
1990
- const matchingPrefix = countMatchingPrefix(existingMessages, nextMessages);
1991
- if (matchingPrefix === existingMessages.length) {
1992
- const newMessages = nextMessages.slice(matchingPrefix);
1993
- if (newMessages.length === 0 && requester && !isDeepStrictEqual(existingRequester, requester)) {
1994
- return [requesterRecordedEntry(requester, sessionId)];
1995
- }
1996
- const requesterIndex = requester ? findLastIndex(newMessages, (m) => m.role === "user") : -1;
1997
- return newMessages.map(
1998
- (message, index) => piEntry(
1999
- message,
2000
- sessionId,
2001
- index === requesterIndex ? requester : void 0
2002
- )
1949
+ function formatConfigurationLines(configuration) {
1950
+ const keys = Object.keys(configuration ?? {}).sort(
1951
+ (a, b) => a.localeCompare(b)
1952
+ );
1953
+ if (keys.length === 0) return null;
1954
+ return keys.map(
1955
+ (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
1956
+ );
1957
+ }
1958
+ var SLACK_HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
1959
+ var LOCAL_HEADER = "You are a helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
1960
+ var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1961
+ var TOOL_POLICY_RULES = [
1962
+ "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
1963
+ "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
1964
+ "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1965
+ "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
1966
+ "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
1967
+ `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
1968
+ "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
1969
+ "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1970
+ "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1971
+ ];
1972
+ var TOOL_CALL_STYLE_RULES = [
1973
+ "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1974
+ "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1975
+ "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
1976
+ "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1977
+ ];
1978
+ var SKILL_POLICY_RULES = [
1979
+ "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1980
+ "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1981
+ ];
1982
+ var EXECUTION_CONTRACT_RULES = [
1983
+ "- Actionable request: act in this turn.",
1984
+ "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
1985
+ "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
1986
+ "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1987
+ "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1988
+ "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
1989
+ ];
1990
+ var CONVERSATION_RULES = [
1991
+ "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1992
+ "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1993
+ "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1994
+ ];
1995
+ var SLACK_ACTION_RULES = [
1996
+ "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1997
+ "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1998
+ "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1999
+ "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
2000
+ "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
2001
+ "- Do not use reactions as progress indicators."
2002
+ ];
2003
+ var SAFETY_RULES = [
2004
+ "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
2005
+ "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
2006
+ "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
2007
+ ];
2008
+ var FAILURE_RULES = [
2009
+ "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
2010
+ "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
2011
+ "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
2012
+ ];
2013
+ function renderRuleSection(tag, lines) {
2014
+ return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
2015
+ }
2016
+ function buildBehaviorSection(platform) {
2017
+ const sections = [
2018
+ renderRuleSection("tool-policy", TOOL_POLICY_RULES),
2019
+ renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
2020
+ renderRuleSection("skill-policy", SKILL_POLICY_RULES),
2021
+ renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
2022
+ renderRuleSection("conversation", CONVERSATION_RULES),
2023
+ renderRuleSection("safety", SAFETY_RULES),
2024
+ renderRuleSection("failure-handling", FAILURE_RULES)
2025
+ ];
2026
+ if (platform === "slack") {
2027
+ sections.splice(
2028
+ 5,
2029
+ 0,
2030
+ renderRuleSection("slack-actions", SLACK_ACTION_RULES)
2003
2031
  );
2004
2032
  }
2033
+ return sections.join("\n\n");
2034
+ }
2035
+ function buildOutputSection(platform) {
2036
+ if (platform === "local") {
2037
+ return [
2038
+ `<output format="markdown">`,
2039
+ "- Start with the answer or result, not internal process narration.",
2040
+ "- Use concise Markdown suitable for terminal output: short paragraphs, bullets, links, and fenced code blocks when helpful.",
2041
+ "- End every turn with a final user-facing response.",
2042
+ "</output>"
2043
+ ].join("\n");
2044
+ }
2045
+ const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
2005
2046
  return [
2006
- resetEntry(
2007
- nextMessages,
2008
- nextSessionId(entries),
2009
- requester ?? existingRequester
2010
- )
2011
- ];
2047
+ openTag,
2048
+ "- Start with the answer or result, not internal process narration.",
2049
+ "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
2050
+ "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
2051
+ "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
2052
+ "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
2053
+ "</output>"
2054
+ ].join("\n");
2012
2055
  }
2013
- function redisStore(redisStateAdapter) {
2014
- const client = redisStateAdapter.getClient();
2015
- return {
2016
- async append({ entries, scope, ttlMs }) {
2017
- const listKey = key(scope);
2018
- if (entries.length > 0) {
2019
- await client.rPush(
2020
- listKey,
2021
- entries.map((entry) => JSON.stringify(entry))
2022
- );
2023
- }
2024
- await client.pExpire(listKey, Math.max(1, ttlMs));
2025
- },
2026
- async read(scope) {
2027
- const values = await client.lRange(key(scope), 0, -1);
2028
- return values.map(decode);
2029
- }
2030
- };
2056
+ function buildIdentitySection(platform) {
2057
+ const name = platform === "slack" ? `Your Slack username is \`${botConfig.userName}\`.` : `Your assistant name is \`${botConfig.userName}\`.`;
2058
+ return ["# Identity", name].join("\n");
2031
2059
  }
2032
- function stateStore() {
2033
- const stateAdapter = getStateAdapter();
2034
- return {
2035
- async append({ entries, scope, ttlMs }) {
2036
- const listKey = rawKey(scope);
2037
- for (const entry of entries) {
2038
- await stateAdapter.appendToList(listKey, entry, {
2039
- ttlMs: Math.max(1, ttlMs)
2040
- });
2041
- }
2042
- },
2043
- async read(scope) {
2044
- const values = await stateAdapter.getList(rawKey(scope));
2045
- return values.map(decode);
2046
- }
2047
- };
2060
+ function buildPersonalitySection() {
2061
+ return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
2048
2062
  }
2049
- async function defaultStore() {
2050
- const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
2051
- if (redisStateAdapter) {
2052
- return redisStore(redisStateAdapter);
2063
+ function buildWorldSection() {
2064
+ if (!JUNIOR_WORLD) {
2065
+ return null;
2066
+ }
2067
+ return ["# World", JUNIOR_WORLD.trim()].join("\n");
2068
+ }
2069
+ function buildRuntimeSection(params) {
2070
+ const lines = [
2071
+ params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
2072
+ params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
2073
+ params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
2074
+ ].filter(Boolean);
2075
+ if (lines.length === 0) {
2076
+ return null;
2077
+ }
2078
+ return renderTagBlock("runtime", lines.join("\n"));
2079
+ }
2080
+ function buildContextSection(params) {
2081
+ const blocks = [];
2082
+ const referenceLines = formatReferenceFilesLines();
2083
+ if (referenceLines) {
2084
+ blocks.push(
2085
+ renderTag("reference-files", [
2086
+ "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
2087
+ ...referenceLines
2088
+ ])
2089
+ );
2090
+ }
2091
+ const requesterLines = renderRequesterBlock({
2092
+ full_name: params.requester?.fullName,
2093
+ user_name: params.requester?.userName,
2094
+ user_id: params.requester?.userId
2095
+ });
2096
+ if (requesterLines) {
2097
+ blocks.push(requesterLines);
2098
+ }
2099
+ const artifactLines = formatArtifactsLines(params.artifactState);
2100
+ if (artifactLines) {
2101
+ blocks.push(renderTag("artifacts", artifactLines));
2102
+ }
2103
+ const configLines = formatConfigurationLines(params.configuration);
2104
+ if (configLines) {
2105
+ blocks.push(
2106
+ renderTag("configuration", [
2107
+ "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
2108
+ ...configLines
2109
+ ])
2110
+ );
2111
+ }
2112
+ if (params.invocation) {
2113
+ blocks.push(
2114
+ renderTag("explicit-skill-trigger", [
2115
+ "Treat this skill as selected. Load it unless the tool says it is unavailable.",
2116
+ `/${escapeXml(params.invocation.skillName)}`
2117
+ ])
2118
+ );
2053
2119
  }
2054
- await stateAdapter.connect();
2055
- return stateStore();
2056
- }
2057
- async function loadEntries(args) {
2058
- const store = args.store ?? await defaultStore();
2059
- return await store.read(args);
2060
- }
2061
- async function loadMessages(args) {
2062
- const messageCount = normalizeMessageCount(args.messageCount);
2063
- if (messageCount === 0) {
2064
- return [];
2120
+ const body = blocks.map((block) => block.join("\n")).join("\n\n");
2121
+ if (!body) {
2122
+ return null;
2065
2123
  }
2066
- const store = args.store ?? await defaultStore();
2067
- const messages = projectMessages(await store.read(args), args.sessionId);
2068
- return messages.length >= messageCount ? messages.slice(0, messageCount) : void 0;
2069
- }
2070
- async function loadProjection(args) {
2071
- const store = args.store ?? await defaultStore();
2072
- return project(await store.read(args), args.sessionId).messages;
2073
- }
2074
- async function loadConnectedMcpProviders(args) {
2075
- return connectedMcpProviders(await loadEntries(args));
2124
+ return renderTagBlock("context", body);
2076
2125
  }
2077
- async function recordMcpProviderConnected(args) {
2078
- const store = args.store ?? await defaultStore();
2079
- const entries = await store.read(args);
2080
- const sessionId = currentSessionId(entries);
2081
- if (connectedMcpProviders(entries).includes(args.provider)) {
2082
- return;
2126
+ function buildCapabilitiesSection(params) {
2127
+ const blocks = [];
2128
+ const availableSkills = formatAvailableSkillsForPrompt(
2129
+ params.availableSkills,
2130
+ params.invocation
2131
+ );
2132
+ if (availableSkills) {
2133
+ blocks.push(availableSkills);
2083
2134
  }
2084
- await store.append({
2085
- scope: args,
2086
- entries: [mcpProviderConnectedEntry(args.provider, sessionId)],
2087
- ttlMs: args.ttlMs
2088
- });
2089
- }
2090
- async function recordAuthorizationRequested(args) {
2091
- const store = args.store ?? await defaultStore();
2092
- const entries = await store.read(args);
2093
- const sessionId = currentSessionId(entries);
2094
- if (projectionEntries(entries).some(
2095
- (entry) => entry.type === "authorization_requested" && entry.authorizationId === args.authorizationId
2096
- )) {
2097
- return;
2135
+ const activeCatalogs = formatActiveMcpCatalogsForPrompt(
2136
+ params.activeMcpCatalogs
2137
+ );
2138
+ if (activeCatalogs) {
2139
+ blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
2098
2140
  }
2099
- await store.append({
2100
- scope: args,
2101
- entries: [
2102
- authorizationRequestedEntry({
2103
- createdAtMs: Date.now(),
2104
- kind: args.kind,
2105
- sessionId,
2106
- provider: args.provider,
2107
- requesterId: args.requesterId,
2108
- authorizationId: args.authorizationId,
2109
- delivery: args.delivery
2110
- })
2111
- ],
2112
- ttlMs: args.ttlMs
2113
- });
2114
- }
2115
- async function recordAuthorizationCompleted(args) {
2116
- const store = args.store ?? await defaultStore();
2117
- const entries = await store.read(args);
2118
- const sessionId = currentSessionId(entries);
2119
- if (projectionEntries(entries).some(
2120
- (entry) => entry.type === "authorization_completed" && entry.authorizationId === args.authorizationId
2121
- )) {
2122
- return;
2141
+ const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
2142
+ if (toolGuidance) {
2143
+ blocks.push(renderTagBlock("tool-guidance", toolGuidance));
2123
2144
  }
2124
- await store.append({
2125
- scope: args,
2126
- entries: [
2127
- authorizationCompletedEntry({
2128
- createdAtMs: Date.now(),
2129
- kind: args.kind,
2130
- sessionId,
2131
- provider: args.provider,
2132
- requesterId: args.requesterId,
2133
- authorizationId: args.authorizationId
2134
- })
2135
- ],
2136
- ttlMs: args.ttlMs
2137
- });
2145
+ if (blocks.length === 0) {
2146
+ return null;
2147
+ }
2148
+ return blocks.join("\n\n");
2138
2149
  }
2139
- async function commitMessages(args) {
2140
- const store = args.store ?? await defaultStore();
2141
- const entries = await store.read(args);
2142
- const existingProjection = project(entries);
2143
- const currentId = currentSessionId(entries);
2144
- const nextEntries = commitEntries(
2145
- existingProjection.messages,
2146
- args.messages,
2147
- currentId,
2148
- entries,
2149
- existingProjection.requester,
2150
- args.requester
2151
- );
2152
- await store.append({
2153
- scope: args,
2154
- entries: nextEntries,
2155
- ttlMs: args.ttlMs
2156
- });
2157
- return {
2158
- sessionId: nextEntries.find((entry) => entry.type === "projection_reset")?.sessionId ?? currentId
2159
- };
2150
+ function buildStaticSystemPrompt(platform) {
2151
+ return [
2152
+ platform === "slack" ? SLACK_HEADER : LOCAL_HEADER,
2153
+ buildIdentitySection(platform),
2154
+ buildPersonalitySection(),
2155
+ buildWorldSection(),
2156
+ buildBehaviorSection(platform),
2157
+ buildOutputSection(platform)
2158
+ ].filter((section) => Boolean(section)).join("\n\n");
2159
+ }
2160
+ var STATIC_SYSTEM_PROMPTS = {
2161
+ local: buildStaticSystemPrompt("local"),
2162
+ slack: buildStaticSystemPrompt("slack")
2163
+ };
2164
+ function buildSystemPrompt(params) {
2165
+ return STATIC_SYSTEM_PROMPTS[params.source.platform];
2166
+ }
2167
+ function buildTurnContextPrompt(params) {
2168
+ const includeSessionContext = params.includeSessionContext ?? true;
2169
+ if (!includeSessionContext) {
2170
+ return null;
2171
+ }
2172
+ const runtimeSections = [
2173
+ buildCapabilitiesSection({
2174
+ availableSkills: params.availableSkills,
2175
+ activeMcpCatalogs: params.activeMcpCatalogs ?? [],
2176
+ invocation: params.invocation,
2177
+ toolGuidance: params.toolGuidance ?? []
2178
+ }),
2179
+ buildContextSection({
2180
+ requester: params.requester,
2181
+ artifactState: params.artifactState,
2182
+ configuration: params.configuration,
2183
+ invocation: params.invocation
2184
+ }),
2185
+ buildRuntimeSection(params.runtime ?? {})
2186
+ ].filter((section) => Boolean(section));
2187
+ if (runtimeSections.length === 0) {
2188
+ return null;
2189
+ }
2190
+ const sections = [
2191
+ `<${TURN_CONTEXT_TAG}>`,
2192
+ TURN_CONTEXT_HEADER,
2193
+ "The current user instruction appears after this block in the same message.",
2194
+ ...runtimeSections,
2195
+ `</${TURN_CONTEXT_TAG}>`
2196
+ ].filter((section) => Boolean(section));
2197
+ return sections.join("\n\n");
2160
2198
  }
2161
2199
 
2162
2200
  // src/chat/state/turn-session.ts
@@ -2638,242 +2676,19 @@ async function failAgentTurnSessionRecord(args) {
2638
2676
  });
2639
2677
  }
2640
2678
 
2641
- // src/chat/sentry-links.ts
2642
- function getSentryOrgSlug() {
2643
- const slug = process.env.SENTRY_ORG_SLUG?.trim();
2644
- return slug || void 0;
2645
- }
2646
- function isSentrySaasDsnHost(host) {
2647
- return host === "sentry.io" || host.endsWith(".sentry.io");
2648
- }
2649
- function buildSentryWebBaseUrl(dsn) {
2650
- if (isSentrySaasDsnHost(dsn.host)) {
2651
- return "https://sentry.io";
2652
- }
2653
- const port = dsn.port ? `:${dsn.port}` : "";
2654
- const path2 = dsn.path ? `/${dsn.path}` : "";
2655
- return `${dsn.protocol}://${dsn.host}${port}${path2}`;
2656
- }
2657
- function buildSentryConversationUrl(conversationId) {
2658
- const client = sentry_exports.getClient();
2659
- const dsn = client?.getDsn();
2660
- if (!dsn?.host || !dsn.projectId) {
2661
- return void 0;
2662
- }
2663
- const orgSlug = getSentryOrgSlug();
2664
- if (!orgSlug) {
2665
- return void 0;
2666
- }
2667
- const encodedId = encodeURIComponent(conversationId);
2668
- const params = new URLSearchParams();
2669
- params.set("project", dsn.projectId);
2670
- const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
2671
- if (isSentrySaasDsnHost(dsn.host)) {
2672
- return `https://${orgSlug}.sentry.io/${path2}`;
2673
- }
2674
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
2675
- }
2676
- function buildSentryTraceUrl(traceId) {
2677
- const client = sentry_exports.getClient();
2678
- const dsn = client?.getDsn();
2679
- if (!dsn?.host || !dsn.projectId) {
2680
- return void 0;
2681
- }
2682
- const orgSlug = getSentryOrgSlug();
2683
- if (!orgSlug) {
2684
- return void 0;
2685
- }
2686
- const encodedTraceId = encodeURIComponent(traceId);
2687
- const params = new URLSearchParams();
2688
- params.set("project", dsn.projectId);
2689
- const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
2690
- if (isSentrySaasDsnHost(dsn.host)) {
2691
- return `https://${orgSlug}.sentry.io/${path2}`;
2692
- }
2693
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
2694
- }
2695
-
2696
- // src/chat/slack/conversation-context.ts
2697
- function normalizeConversationName(type, channelName) {
2698
- const trimmed = channelName?.trim();
2699
- if (!trimmed) return void 0;
2700
- if (type === "public_channel" || type === "private_channel" || type === "private_channel_or_group_dm") {
2701
- return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
2702
- }
2703
- return trimmed;
2704
- }
2705
- function typeFromSlackChannelType(channelType) {
2706
- if (channelType === "channel") return "public_channel";
2707
- if (channelType === "group") return "private_channel";
2708
- if (channelType === "mpim") return "group_dm";
2709
- if (channelType === "im") return "direct_message";
2710
- return void 0;
2711
- }
2712
- function typeFromChannelId(channelId, channelName) {
2713
- const normalized = normalizeSlackConversationId(channelId);
2714
- if (!normalized) return void 0;
2715
- if (normalized.startsWith("C")) return "public_channel";
2716
- if (normalized.startsWith("D")) return "direct_message";
2717
- if (normalized.startsWith("G")) {
2718
- return channelName?.trim().startsWith("mpdm-") ? "group_dm" : "private_channel_or_group_dm";
2719
- }
2720
- return void 0;
2721
- }
2722
- function toSlackEventChannelType(channelType) {
2723
- if (channelType === "channel" || channelType === "group" || channelType === "mpim" || channelType === "im") {
2724
- return channelType;
2725
- }
2726
- return void 0;
2727
- }
2728
- function resolveSlackChannelTypeFromMessage(message) {
2729
- const raw = message.raw;
2730
- if (!raw || typeof raw !== "object") {
2731
- return void 0;
2732
- }
2733
- const channelType = raw.channel_type;
2734
- return typeof channelType === "string" ? toSlackEventChannelType(channelType.trim()) : void 0;
2735
- }
2736
- function resolveSlackConversationContext(input) {
2737
- const type = typeFromSlackChannelType(input.channelType) ?? typeFromChannelId(input.channelId, input.channelName);
2738
- if (!type) return void 0;
2739
- const name = normalizeConversationName(type, input.channelName);
2740
- return {
2741
- type,
2742
- ...name ? { name } : {}
2743
- };
2744
- }
2745
- function resolveSlackConversationContextFromThreadId(input) {
2746
- const slackThread = parseSlackThreadId(input.threadId);
2747
- return resolveSlackConversationContext({
2748
- channelId: slackThread?.channelId,
2749
- channelName: input.channelName
2750
- });
2751
- }
2752
- function formatSlackConversationTypeLabel(type) {
2753
- if (type === "public_channel") return "Public Channel";
2754
- if (type === "private_channel") return "Private Channel";
2755
- if (type === "group_dm") return "Group DM";
2756
- if (type === "direct_message") return "Direct Message";
2757
- return "Private Channel or Group DM";
2758
- }
2759
- function formatSlackConversationRedactedLabel(context) {
2760
- if (!context) return void 0;
2761
- return formatSlackConversationTypeLabel(context.type);
2762
- }
2763
-
2764
- // src/chat/state/conversation-details.ts
2765
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
2766
- var CONVERSATION_PREFIX = "junior:conversation";
2767
- var CONVERSATION_DETAILS_TTL_MS = THREAD_STATE_TTL_MS2;
2768
- function conversationContextKey(conversationId) {
2769
- return `${CONVERSATION_PREFIX}:${conversationId}:context`;
2770
- }
2771
- function conversationTitleKey(conversationId) {
2772
- return `${CONVERSATION_PREFIX}:${conversationId}:title`;
2773
- }
2774
- function parseOriginRequester(value) {
2775
- return parseStoredSlackRequester(value);
2776
- }
2777
- function parseOriginSurface(value) {
2778
- if (value === "slack" || value === "api" || value === "scheduler" || value === "internal") {
2779
- return value;
2780
- }
2781
- return void 0;
2782
- }
2783
- function storedContextFromInput(context) {
2784
- return {
2785
- ...context.channelName ? { channelName: context.channelName } : {},
2786
- ...context.originSurface ? { originSurface: context.originSurface } : {},
2787
- ...context.originRequester ? { originRequester: context.originRequester } : {},
2788
- startedAtMs: context.startedAtMs
2789
- };
2790
- }
2791
- function parseContext(value) {
2792
- if (!isRecord(value)) return void 0;
2793
- const startedAtMs = toOptionalNumber(value.startedAtMs);
2794
- if (startedAtMs === void 0) return void 0;
2795
- return {
2796
- ...typeof value.channelName === "string" && value.channelName.trim() ? { channelName: value.channelName.trim() } : {},
2797
- ...parseOriginSurface(value.originSurface) ? { originSurface: parseOriginSurface(value.originSurface) } : {},
2798
- ...parseOriginRequester(value.originRequester) ? { originRequester: parseOriginRequester(value.originRequester) } : {},
2799
- startedAtMs
2800
- };
2801
- }
2802
- function parseTitle(value) {
2803
- if (!isRecord(value)) return void 0;
2804
- const displayTitle = typeof value.displayTitle === "string" && value.displayTitle.trim() ? value.displayTitle.trim() : void 0;
2805
- if (!displayTitle) return void 0;
2806
- return {
2807
- displayTitle,
2808
- ...typeof value.titleSourceMessageId === "string" ? { titleSourceMessageId: value.titleSourceMessageId } : {}
2809
- };
2810
- }
2811
- async function initConversationContext(conversationId, context) {
2812
- const stateAdapter = getStateAdapter();
2813
- await stateAdapter.connect();
2814
- const key2 = conversationContextKey(conversationId);
2815
- const inserted = await stateAdapter.setIfNotExists(
2816
- key2,
2817
- storedContextFromInput(context),
2818
- CONVERSATION_DETAILS_TTL_MS
2819
- );
2820
- if (inserted) return;
2821
- const existing = parseContext(await stateAdapter.get(key2));
2822
- if (!existing) {
2823
- return;
2824
- }
2825
- await stateAdapter.set(key2, existing, CONVERSATION_DETAILS_TTL_MS);
2826
- }
2827
- async function setConversationTitle(conversationId, title) {
2828
- const stateAdapter = getStateAdapter();
2829
- await stateAdapter.connect();
2830
- await stateAdapter.set(
2831
- conversationTitleKey(conversationId),
2832
- {
2833
- displayTitle: title.displayTitle,
2834
- ...title.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {}
2835
- },
2836
- CONVERSATION_DETAILS_TTL_MS
2837
- );
2838
- }
2839
- async function getConversationDetails(conversationId) {
2840
- const stateAdapter = getStateAdapter();
2841
- await stateAdapter.connect();
2842
- const [rawContext, rawTitle] = await Promise.all([
2843
- stateAdapter.get(conversationContextKey(conversationId)),
2844
- stateAdapter.get(conversationTitleKey(conversationId))
2845
- ]);
2846
- const context = parseContext(rawContext);
2847
- const title = parseTitle(rawTitle);
2848
- if (!context && !title) return void 0;
2849
- return {
2850
- conversationId,
2851
- ...title?.displayTitle ? { displayTitle: title.displayTitle } : {},
2852
- ...title?.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {},
2853
- ...context?.channelName ? { channelName: context.channelName } : {},
2854
- ...context?.originSurface ? { originSurface: context.originSurface } : {},
2855
- ...context?.originRequester ? { originRequester: context.originRequester } : {},
2856
- ...context?.startedAtMs !== void 0 ? { startedAtMs: context.startedAtMs } : {}
2857
- };
2858
- }
2859
- async function getConversationDetailsForIds(conversationIds) {
2860
- const uniqueIds = [...new Set(conversationIds)].filter(Boolean);
2861
- const entries = await Promise.all(
2862
- uniqueIds.map(async (id) => {
2863
- const details = await getConversationDetails(id);
2864
- return details ? [id, details] : void 0;
2865
- })
2866
- );
2867
- const result = /* @__PURE__ */ new Map();
2868
- for (const entry of entries) {
2869
- if (entry) result.set(entry[0], entry[1]);
2870
- }
2871
- return result;
2872
- }
2873
-
2874
2679
  export {
2680
+ getInterruptionMarker,
2681
+ truncateStatusText,
2682
+ normalizeSlackStatusText,
2683
+ splitSlackReplyText,
2684
+ buildSlackOutputMessage,
2685
+ escapeXml,
2686
+ JUNIOR_PERSONALITY,
2687
+ buildSystemPrompt,
2688
+ buildTurnContextPrompt,
2875
2689
  createAgentPluginLogger,
2876
2690
  createPluginState,
2691
+ getSlackToolContext,
2877
2692
  bindSlackDirectCredentialSubject,
2878
2693
  verifySlackDirectCredentialSubject,
2879
2694
  resolveChannelCapabilities,
@@ -2885,16 +2700,6 @@ export {
2885
2700
  getAgentPluginSlackConversationLink,
2886
2701
  getAgentPluginOperationalReports,
2887
2702
  createAgentPluginHookRunner,
2888
- GET,
2889
- getInterruptionMarker,
2890
- truncateStatusText,
2891
- normalizeSlackStatusText,
2892
- splitSlackReplyText,
2893
- buildSlackOutputMessage,
2894
- escapeXml,
2895
- JUNIOR_PERSONALITY,
2896
- buildSystemPrompt,
2897
- buildTurnContextPrompt,
2898
2703
  loadProjection,
2899
2704
  loadConnectedMcpProviders,
2900
2705
  recordMcpProviderConnected,
@@ -2906,15 +2711,5 @@ export {
2906
2711
  recordAgentTurnSessionSummary,
2907
2712
  listAgentTurnSessionSummariesForConversation,
2908
2713
  abandonAgentTurnSessionRecord,
2909
- failAgentTurnSessionRecord,
2910
- buildSentryConversationUrl,
2911
- buildSentryTraceUrl,
2912
- resolveSlackChannelTypeFromMessage,
2913
- resolveSlackConversationContext,
2914
- resolveSlackConversationContextFromThreadId,
2915
- formatSlackConversationRedactedLabel,
2916
- initConversationContext,
2917
- setConversationTitle,
2918
- getConversationDetails,
2919
- getConversationDetailsForIds
2714
+ failAgentTurnSessionRecord
2920
2715
  };