@sentry/junior 0.71.3 → 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 (103) hide show
  1. package/bin/junior.mjs +22 -10
  2. package/dist/api-reference.d.ts +2 -0
  3. package/dist/app.d.ts +12 -3
  4. package/dist/app.js +2522 -15560
  5. package/dist/chat/agent-dispatch/heartbeat.d.ts +0 -6
  6. package/dist/chat/agent-dispatch/runner.d.ts +2 -0
  7. package/dist/chat/agent-dispatch/store.d.ts +2 -2
  8. package/dist/chat/agent-dispatch/types.d.ts +6 -3
  9. package/dist/chat/agent-dispatch/validation.d.ts +2 -3
  10. package/dist/chat/app/production.d.ts +8 -1
  11. package/dist/chat/app/services.d.ts +7 -0
  12. package/dist/chat/destination.d.ts +3 -1
  13. package/dist/chat/logging.d.ts +3 -0
  14. package/dist/chat/mcp/errors.d.ts +3 -0
  15. package/dist/chat/mcp/tool-manager.d.ts +5 -1
  16. package/dist/chat/oauth-flow.d.ts +1 -1
  17. package/dist/chat/plugins/agent-hooks.d.ts +3 -2
  18. package/dist/chat/plugins/registry.d.ts +2 -0
  19. package/dist/chat/plugins/types.d.ts +2 -0
  20. package/dist/chat/prompt.d.ts +4 -1
  21. package/dist/chat/requester.d.ts +67 -0
  22. package/dist/chat/respond.d.ts +8 -8
  23. package/dist/chat/runtime/agent-continue-runner.d.ts +25 -0
  24. package/dist/chat/runtime/reply-executor.d.ts +7 -5
  25. package/dist/chat/runtime/slack-resume.d.ts +3 -3
  26. package/dist/chat/runtime/slack-runtime.d.ts +13 -3
  27. package/dist/chat/runtime/turn.d.ts +17 -3
  28. package/dist/chat/sandbox/egress-credentials.d.ts +5 -2
  29. package/dist/chat/sandbox/egress-policy.d.ts +5 -1
  30. package/dist/chat/sandbox/egress-proxy.d.ts +2 -0
  31. package/dist/chat/sandbox/egress-schemas.d.ts +4 -0
  32. package/dist/chat/sandbox/egress-session.d.ts +3 -1
  33. package/dist/chat/sandbox/egress-tracing.d.ts +7 -0
  34. package/dist/chat/sandbox/sandbox.d.ts +2 -0
  35. package/dist/chat/sandbox/session.d.ts +3 -2
  36. package/dist/chat/services/agent-continue.d.ts +27 -0
  37. package/dist/chat/services/auth-pause-response.d.ts +1 -1
  38. package/dist/chat/services/auth-pause.d.ts +2 -1
  39. package/dist/chat/services/mcp-auth-orchestration.d.ts +1 -1
  40. package/dist/chat/services/message-actor-identity.d.ts +12 -4
  41. package/dist/chat/services/plugin-auth-orchestration.d.ts +9 -8
  42. package/dist/chat/services/turn-result.d.ts +3 -0
  43. package/dist/chat/services/turn-session-record.d.ts +10 -7
  44. package/dist/chat/slack/user.d.ts +4 -4
  45. package/dist/chat/state/adapter.d.ts +2 -0
  46. package/dist/chat/state/conversation-details.d.ts +4 -3
  47. package/dist/chat/state/session-log.d.ts +43 -0
  48. package/dist/chat/state/turn-session.d.ts +7 -10
  49. package/dist/chat/task-execution/slack-work.d.ts +5 -5
  50. package/dist/chat/task-execution/store.d.ts +83 -48
  51. package/dist/chat/task-execution/worker.d.ts +3 -3
  52. package/dist/chat/tools/definition.d.ts +3 -0
  53. package/dist/chat/tools/execution/tool-error-handler.d.ts +2 -1
  54. package/dist/chat/tools/slack/canvas-tools.d.ts +3 -2
  55. package/dist/chat/tools/slack/channel-list-messages.d.ts +2 -2
  56. package/dist/chat/tools/slack/channel-post-message.d.ts +3 -2
  57. package/dist/chat/tools/slack/context.d.ts +15 -2
  58. package/dist/chat/tools/slack/message-add-reaction.d.ts +3 -2
  59. package/dist/chat/tools/slack/thread-read.d.ts +2 -2
  60. package/dist/chat/tools/types.d.ts +20 -23
  61. package/dist/{chunk-BBXYXOJW.js → chunk-3BYAPS6B.js} +48 -529
  62. package/dist/{chunk-UXG6TU2U.js → chunk-7Q5YOUUT.js} +16 -93
  63. package/dist/chunk-AL5T52ZD.js +1119 -0
  64. package/dist/chunk-CYUI7JU5.js +195 -0
  65. package/dist/{chunk-R62YWUNO.js → chunk-DIMX5F3T.js} +10 -28
  66. package/dist/chunk-G3E7SCME.js +28 -0
  67. package/dist/chunk-KVZL5NZS.js +519 -0
  68. package/dist/chunk-M4FLLXXD.js +212 -0
  69. package/dist/chunk-OQSYYOLM.js +12787 -0
  70. package/dist/{chunk-GT67ZWZQ.js → chunk-OR6NQJ5E.js} +5 -3
  71. package/dist/{chunk-B5HKWWQB.js → chunk-RY6AL5C7.js} +8 -6
  72. package/dist/chunk-SJHUF3DP.js +43 -0
  73. package/dist/{chunk-XE2VFQQN.js → chunk-UOTZ3EEQ.js} +1 -1
  74. package/dist/{chunk-HOGQL2H6.js → chunk-UZVHXZ7V.js} +1357 -1486
  75. package/dist/{chunk-76YMBKW7.js → chunk-V4VYUY4A.js} +27 -14
  76. package/dist/{chunk-JS4HURDT.js → chunk-WS2EG3GW.js} +224 -224
  77. package/dist/chunk-ZDA2HYX5.js +275 -0
  78. package/dist/cli/chat.js +205 -0
  79. package/dist/cli/check.js +6 -5
  80. package/dist/cli/run.js +18 -2
  81. package/dist/cli/snapshot-warmup.js +11 -8
  82. package/dist/cli/upgrade.js +599 -0
  83. package/dist/deployment.d.ts +4 -0
  84. package/dist/handlers/agent-dispatch.d.ts +6 -1
  85. package/dist/handlers/mcp-oauth-callback.d.ts +6 -1
  86. package/dist/handlers/oauth-callback.d.ts +6 -1
  87. package/dist/handlers/sandbox-egress-proxy.d.ts +2 -0
  88. package/dist/handlers/webhooks.d.ts +4 -2
  89. package/dist/instrumentation.js +17 -2
  90. package/dist/nitro.d.ts +1 -1
  91. package/dist/nitro.js +10 -10
  92. package/dist/plugins.d.ts +1 -1
  93. package/dist/reporting/conversations.d.ts +116 -0
  94. package/dist/reporting.d.ts +24 -129
  95. package/dist/reporting.js +320 -166
  96. package/dist/runner-LMAM4OGD.js +259 -0
  97. package/package.json +3 -3
  98. package/dist/chat/runtime/timeout-resume-runner.d.ts +0 -19
  99. package/dist/chat/services/requester-identity.d.ts +0 -19
  100. package/dist/chat/services/timeout-resume.d.ts +0 -23
  101. package/dist/chunk-6YY4Q3D4.js +0 -12
  102. package/dist/chunk-Z3YD6NHK.js +0 -12
  103. package/dist/handlers/turn-resume.d.ts +0 -4
@@ -1,40 +1,44 @@
1
1
  import {
2
- SANDBOX_DATA_ROOT,
3
- SANDBOX_WORKSPACE_ROOT,
4
- getConnectedStateContext,
5
- getStateAdapter,
6
- sandboxSkillDir
7
- } from "./chunk-R62YWUNO.js";
8
- import {
9
- isActorUserId,
10
- parseActorUserId
11
- } from "./chunk-UXG6TU2U.js";
2
+ recordConversationActivity
3
+ } from "./chunk-AL5T52ZD.js";
12
4
  import {
13
5
  isConversationChannel,
14
6
  isConversationScopedChannel,
15
7
  isDmChannel,
16
8
  normalizeSlackConversationId,
17
9
  parseDestination
18
- } from "./chunk-76YMBKW7.js";
10
+ } from "./chunk-V4VYUY4A.js";
11
+ import {
12
+ SANDBOX_DATA_ROOT,
13
+ SANDBOX_WORKSPACE_ROOT,
14
+ sandboxSkillDir
15
+ } from "./chunk-G3E7SCME.js";
16
+ import {
17
+ listReferenceFiles,
18
+ soulPathCandidates,
19
+ worldPathCandidates
20
+ } from "./chunk-KVZL5NZS.js";
21
+ import {
22
+ getConnectedStateContext,
23
+ getStateAdapter
24
+ } from "./chunk-DIMX5F3T.js";
19
25
  import {
20
26
  TURN_CONTEXT_TAG,
21
27
  botConfig,
22
- getChatConfig,
23
- parseSlackThreadId
24
- } from "./chunk-JS4HURDT.js";
28
+ getChatConfig
29
+ } from "./chunk-WS2EG3GW.js";
30
+ import {
31
+ isActorUserId,
32
+ parseActorUserId,
33
+ parseStoredSlackRequester,
34
+ storedSlackRequesterSchema
35
+ } from "./chunk-CYUI7JU5.js";
25
36
  import {
26
37
  isRecord,
27
- listReferenceFiles,
28
38
  logException,
29
39
  logInfo,
30
- logWarn,
31
- soulPathCandidates,
32
- toOptionalNumber,
33
- worldPathCandidates
34
- } from "./chunk-BBXYXOJW.js";
35
- import {
36
- sentry_exports
37
- } from "./chunk-Z3YD6NHK.js";
40
+ logWarn
41
+ } from "./chunk-3BYAPS6B.js";
38
42
 
39
43
  // src/chat/plugins/logging.ts
40
44
  function createAgentPluginLogger(plugin) {
@@ -160,6 +164,23 @@ function createPluginState(plugin, options) {
160
164
  };
161
165
  }
162
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
+
163
184
  // src/chat/credentials/subject.ts
164
185
  import { createHmac, timingSafeEqual } from "crypto";
165
186
  var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
@@ -351,29 +372,43 @@ function getAgentPluginTools(context) {
351
372
  }
352
373
  const log = createAgentPluginLogger(plugin.name);
353
374
  const destination = context.destination;
354
- const credentialSubject = createSlackDirectCredentialSubject({
355
- channelId: context.channelId,
356
- teamId: context.teamId,
357
- userId: context.requester?.userId
358
- });
359
- const pluginCapabilities = resolveChannelCapabilities(context.channelId);
360
- 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" ? {
361
388
  plugin: { name: plugin.name },
362
389
  log,
363
- requester: context.requester,
364
- channelCapabilities: pluginCapabilities,
365
- channelId: context.channelId,
390
+ requester: context.requester?.platform === "slack" ? context.requester : void 0,
366
391
  conversationId: context.conversationId,
367
- ...credentialSubject ? { credentialSubject } : {},
368
- ...destination ? { destination } : {},
369
- teamId: context.teamId,
370
- messageTs: context.messageTs,
371
- threadTs: context.threadTs,
392
+ destination: destination?.platform === "slack" ? destination : void 0,
393
+ slack: slackContext,
394
+ source: context.source,
372
395
  userText: context.userText,
373
396
  state: createPluginState(plugin.name, {
374
397
  legacyStatePrefixes: plugin.legacyStatePrefixes
375
398
  })
376
- });
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);
377
412
  for (const [name, tool] of Object.entries(pluginTools)) {
378
413
  if (!AGENT_PLUGIN_TOOL_NAME_RE.test(name)) {
379
414
  throw new Error(
@@ -809,1293 +844,1361 @@ function createAgentPluginHookRunner(input = {}) {
809
844
  };
810
845
  }
811
846
 
812
- // src/handlers/health.ts
813
- function GET() {
814
- return Response.json({
815
- status: "ok",
816
- service: "junior",
817
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
818
- });
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(":");
819
926
  }
820
-
821
- // src/chat/prompt.ts
822
- import fs from "fs";
823
- import path from "path";
824
-
825
- // src/chat/interruption-marker.ts
826
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
827
- function getInterruptionMarker() {
828
- return INTERRUPTED_MARKER;
927
+ function rawKey(scope) {
928
+ return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
829
929
  }
830
-
831
- // src/chat/slack/status-format.ts
832
- var SLACK_STATUS_MAX_LENGTH = 50;
833
- function truncateStatusText(text) {
834
- const trimmed = text.trim();
835
- if (!trimmed) {
836
- return "";
837
- }
838
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
839
- return trimmed;
840
- }
841
- 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;
842
932
  }
843
-
844
- // src/chat/slack/mrkdwn.ts
845
- function readInlineCodeSpan(line, start) {
846
- if (line[start] !== "`") {
847
- return void 0;
848
- }
849
- let n = 1;
850
- while (line[start + n] === "`") {
851
- n++;
852
- }
853
- const marker = "`".repeat(n);
854
- let search = start + n;
855
- while (search < line.length) {
856
- const close = line.indexOf(marker, search);
857
- if (close === -1) {
858
- return void 0;
859
- }
860
- const after = close + n;
861
- if (line[after] !== "`") {
862
- 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;
863
938
  }
864
- search = after + 1;
865
939
  }
866
- return void 0;
940
+ return limit;
867
941
  }
868
- function readExistingSlackAngleToken(line, start) {
869
- if (line[start] !== "<") {
870
- return void 0;
871
- }
872
- const close = line.indexOf(">", start + 1);
873
- if (close === -1) {
874
- return void 0;
875
- }
876
- const body = line.slice(start + 1, close);
877
- if (/^(?:https?:\/\/|@|#|!)/.test(body)) {
878
- return { text: line.slice(start, close + 1), end: close + 1 };
879
- }
880
- return void 0;
942
+ function entrySessionId(entry) {
943
+ return entry.sessionId ?? INITIAL_SESSION_ID;
881
944
  }
882
- function readMarkdownLink(line, start) {
883
- if (line[start] !== "[") {
884
- return void 0;
885
- }
886
- const labelEnd = line.indexOf("](", start + 1);
887
- if (labelEnd === -1) {
888
- return void 0;
889
- }
890
- const destStart = labelEnd + 2;
891
- if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) {
892
- return void 0;
893
- }
894
- const closeParens = line.indexOf(")", destStart);
895
- if (closeParens === -1) {
896
- 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
+ }
897
950
  }
898
- return { text: line.slice(start, closeParens + 1), end: closeParens + 1 };
951
+ return -1;
899
952
  }
900
- function hasUnmatchedClosingParen(text) {
901
- let balance = 0;
902
- for (const ch of text) {
903
- if (ch === "(") balance++;
904
- else if (ch === ")") balance--;
953
+ function currentSessionId(entries) {
954
+ const resetIndex = latestProjectionResetIndex(entries);
955
+ if (resetIndex < 0) {
956
+ return INITIAL_SESSION_ID;
905
957
  }
906
- return balance < 0;
958
+ return entrySessionId(entries[resetIndex]);
907
959
  }
908
- function readBareUrl(line, start) {
909
- let end = start;
910
- while (end < line.length) {
911
- const ch = line[end];
912
- if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === '"' || ch === "`" || ch === "|" || ch === "*") {
913
- 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
+ }
914
983
  }
915
- end++;
984
+ return sessionEntries;
916
985
  }
917
- if (end === start) {
918
- return void 0;
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);
990
+ }
991
+ function findLastIndex(values, predicate) {
992
+ for (let index = values.length - 1; index >= 0; index -= 1) {
993
+ if (predicate(values[index])) return index;
919
994
  }
920
- let raw = line.slice(start, end);
921
- let suffix = "";
922
- const peel = () => {
923
- suffix = raw.slice(-1) + suffix;
924
- raw = raw.slice(0, -1);
925
- };
926
- const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
927
- while (raw.length > 0 && shouldPeel()) {
928
- peel();
995
+ return -1;
996
+ }
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
+ };
1005
+ }
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
+ };
1014
+ }
1015
+ function requesterRecordedEntry(requester, sessionId) {
1016
+ return {
1017
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1018
+ type: "requester_recorded",
1019
+ sessionId,
1020
+ requester
1021
+ };
1022
+ }
1023
+ function mcpProviderConnectedEntry(provider, sessionId) {
1024
+ return {
1025
+ schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1026
+ type: "mcp_provider_connected",
1027
+ sessionId,
1028
+ provider
1029
+ };
1030
+ }
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
+ };
1043
+ }
1044
+ function authorizationRequestedEntry(args) {
1045
+ return {
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
1055
+ };
1056
+ }
1057
+ function authorizationCompletedEntry(args) {
1058
+ return {
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
1067
+ };
1068
+ }
1069
+ function decode(value) {
1070
+ if (typeof value === "string") {
1071
+ return decode(JSON.parse(value));
929
1072
  }
930
- if (!/^https?:\/\/.+/.test(raw)) {
931
- return void 0;
1073
+ const parsed = sessionLogEntrySchema.safeParse(value);
1074
+ if (parsed.success) {
1075
+ return parsed.data;
932
1076
  }
933
- return { url: raw, suffix, end };
1077
+ return piEntry(piMessageSchema.parse(value), INITIAL_SESSION_ID);
934
1078
  }
935
- function wrapBareUrlsOnLine(line) {
936
- let result = "";
937
- let i = 0;
938
- while (i < line.length) {
939
- const codeSpan = readInlineCodeSpan(line, i);
940
- if (codeSpan) {
941
- result += codeSpan.text;
942
- i = codeSpan.end;
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
+ }
943
1088
  continue;
944
1089
  }
945
- const angleToken = readExistingSlackAngleToken(line, i);
946
- if (angleToken) {
947
- result += angleToken.text;
948
- i = angleToken.end;
1090
+ if (entry.type === "requester_recorded") {
1091
+ requester = entry.requester;
949
1092
  continue;
950
1093
  }
951
- const mdLink = readMarkdownLink(line, i);
952
- if (mdLink) {
953
- result += mdLink.text;
954
- i = mdLink.end;
1094
+ if (entry.type === "authorization_completed") {
1095
+ messages.push(authorizationObservationMessage(entry));
955
1096
  continue;
956
1097
  }
957
- if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
958
- const parsed = readBareUrl(line, i);
959
- if (parsed) {
960
- result += `<${parsed.url}>${parsed.suffix}`;
961
- i = parsed.end;
962
- continue;
963
- }
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;
964
1104
  }
965
- result += line[i];
966
- i++;
967
1105
  }
968
- return result;
1106
+ return { messages, requester };
969
1107
  }
970
- function wrapBareUrls(text) {
971
- const lines = text.split("\n");
972
- const out = [];
973
- let inCodeBlock = false;
974
- for (const line of lines) {
975
- if (line.trimStart().startsWith("```")) {
976
- inCodeBlock = !inCodeBlock;
977
- out.push(line);
978
- continue;
1108
+ function projectMessages(entries, sessionId) {
1109
+ return project(entries, sessionId).messages;
1110
+ }
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);
979
1116
  }
980
- out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
981
1117
  }
982
- return out.join("\n");
1118
+ return [...providers].sort((left, right) => left.localeCompare(right));
983
1119
  }
984
- function ensureBlockSpacing(text) {
985
- const codeBlockPattern = /^```/;
986
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
987
- const lines = text.split("\n");
988
- const result = [];
989
- let inCodeBlock = false;
990
- for (let i = 0; i < lines.length; i++) {
991
- const line = lines[i];
992
- const isCodeFence = codeBlockPattern.test(line.trimStart());
993
- if (isCodeFence) {
994
- if (!inCodeBlock) {
995
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
996
- if (prev2 !== void 0 && prev2.trim() !== "") {
997
- result.push("");
998
- }
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)];
1126
+ }
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();
1146
+ return {
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
+ );
999
1154
  }
1000
- inCodeBlock = !inCodeBlock;
1001
- result.push(line);
1002
- continue;
1003
- }
1004
- if (inCodeBlock) {
1005
- result.push(line);
1006
- continue;
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);
1007
1160
  }
1008
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
1009
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
1010
- result.push("");
1161
+ };
1162
+ }
1163
+ function stateStore() {
1164
+ const stateAdapter = getStateAdapter();
1165
+ return {
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);
1011
1177
  }
1012
- result.push(line);
1178
+ };
1179
+ }
1180
+ async function defaultStore() {
1181
+ const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
1182
+ if (redisStateAdapter) {
1183
+ return redisStore(redisStateAdapter);
1013
1184
  }
1014
- return result.join("\n");
1185
+ await stateAdapter.connect();
1186
+ return stateStore();
1015
1187
  }
1016
- function normalizeSlackReplyMarkdown(text) {
1017
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1018
- normalized = wrapBareUrls(normalized);
1019
- normalized = ensureBlockSpacing(normalized);
1020
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
1188
+ async function loadEntries(args) {
1189
+ const store = args.store ?? await defaultStore();
1190
+ return await store.read(args);
1021
1191
  }
1022
- function normalizeSlackStatusText(text) {
1023
- const trimmed = text.trim();
1024
- if (!trimmed) {
1025
- return "";
1192
+ async function loadMessages(args) {
1193
+ const messageCount = normalizeMessageCount(args.messageCount);
1194
+ if (messageCount === 0) {
1195
+ return [];
1026
1196
  }
1027
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
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;
1028
1200
  }
1029
-
1030
- // src/chat/slack/output.ts
1031
- var MAX_INLINE_CHARS = 2200;
1032
- var MAX_INLINE_LINES = 45;
1033
- var CONTINUED_MARKER = "\n\n[Continued below]";
1034
- function countSlackLines(text) {
1035
- if (!text) {
1036
- return 0;
1037
- }
1038
- return text.split("\n").length;
1201
+ async function loadProjection(args) {
1202
+ const store = args.store ?? await defaultStore();
1203
+ return project(await store.read(args), args.sessionId).messages;
1039
1204
  }
1040
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1041
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
1205
+ async function loadConnectedMcpProviders(args) {
1206
+ return connectedMcpProviders(await loadEntries(args));
1042
1207
  }
1043
- function findSplitIndex(text, maxChars) {
1044
- if (text.length <= maxChars) {
1045
- return text.length;
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;
1046
1214
  }
1047
- const bounded = text.slice(0, maxChars);
1048
- const newlineIndex = bounded.lastIndexOf("\n");
1049
- if (newlineIndex > 0) {
1050
- return newlineIndex;
1051
- }
1052
- const spaceIndex = bounded.lastIndexOf(" ");
1053
- if (spaceIndex > 0) {
1054
- return spaceIndex;
1055
- }
1056
- return maxChars;
1215
+ await store.append({
1216
+ scope: args,
1217
+ entries: [mcpProviderConnectedEntry(args.provider, sessionId)],
1218
+ ttlMs: args.ttlMs
1219
+ });
1057
1220
  }
1058
- function splitByLineBudget(text, maxLines) {
1059
- if (maxLines <= 0) {
1060
- return "";
1061
- }
1062
- const lines = text.split("\n");
1063
- if (lines.length <= maxLines) {
1064
- return text;
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;
1065
1229
  }
1066
- return lines.slice(0, maxLines).join("\n");
1067
- }
1068
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1069
- return {
1070
- maxChars: Math.max(1, maxChars - suffix.length),
1071
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1072
- };
1073
- }
1074
- function forceSplitBudget(text, budget) {
1075
- const lineCount = countSlackLines(text);
1076
- return {
1077
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1078
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1079
- };
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
+ });
1080
1245
  }
1081
- function getFenceContinuation(text) {
1082
- let open;
1083
- for (const line of text.split("\n")) {
1084
- const trimmed = line.trimStart();
1085
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1086
- if (!openerMatch) {
1087
- continue;
1088
- }
1089
- if (!open) {
1090
- open = {
1091
- fence: openerMatch[1],
1092
- openerLine: trimmed
1093
- };
1094
- continue;
1095
- }
1096
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1097
- open = void 0;
1098
- }
1099
- }
1100
- if (!open) {
1101
- 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;
1102
1254
  }
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
+ });
1269
+ }
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
+ });
1103
1288
  return {
1104
- closeSuffix: text.endsWith("\n") ? open.fence : `
1105
- ${open.fence}`,
1106
- reopenPrefix: `${open.openerLine}
1107
- `
1289
+ sessionId: nextEntries.find((entry) => entry.type === "projection_reset")?.sessionId ?? currentId
1108
1290
  };
1109
1291
  }
1110
- function appendSlackSuffix(text, marker) {
1111
- const carryover = getFenceContinuation(text);
1112
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1292
+
1293
+ // src/chat/xml.ts
1294
+ function escapeXml(value) {
1295
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1113
1296
  }
1114
- function stripTrailingContinuationMarker(text) {
1115
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
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;
1116
1306
  }
1117
- function takeSlackContinuationChunk(text, budget) {
1118
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1119
- if (!rest) {
1120
- ({ prefix, rest } = takeSlackInlinePrefix(
1121
- text,
1122
- forceSplitBudget(text, budget)
1123
- ));
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 "";
1124
1314
  }
1125
- let carryover = rest ? getFenceContinuation(prefix) : null;
1126
- if (!carryover) {
1127
- return { prefix, rest };
1315
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
1316
+ return trimmed;
1128
1317
  }
1129
- const carryoverBudget = reserveInlineBudgetForSuffix(
1130
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
1131
- );
1132
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1133
- if (!rest) {
1134
- ({ prefix, rest } = takeSlackInlinePrefix(
1135
- text,
1136
- forceSplitBudget(text, carryoverBudget)
1137
- ));
1318
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
1319
+ }
1320
+
1321
+ // src/chat/slack/mrkdwn.ts
1322
+ function readInlineCodeSpan(line, start) {
1323
+ if (line[start] !== "`") {
1324
+ return void 0;
1138
1325
  }
1139
- carryover = rest ? getFenceContinuation(prefix) : null;
1140
- if (!carryover) {
1141
- return { prefix, rest };
1326
+ let n = 1;
1327
+ while (line[start + n] === "`") {
1328
+ n++;
1142
1329
  }
1143
- return {
1144
- prefix,
1145
- rest: `${carryover.reopenPrefix}${rest}`
1146
- };
1147
- }
1148
- function takeSlackContinuationPrefix(text, options) {
1149
- const budget = {
1150
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1151
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1152
- };
1153
- const { prefix, rest } = (() => {
1154
- if (options?.forceSplit) {
1155
- return takeSlackContinuationChunk(text, budget);
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;
1156
1336
  }
1157
- const initial = takeSlackInlinePrefix(text, budget);
1158
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1159
- })();
1160
- return {
1161
- prefix,
1162
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1163
- rest
1164
- };
1337
+ const after = close + n;
1338
+ if (line[after] !== "`") {
1339
+ return { text: line.slice(start, after), end: after };
1340
+ }
1341
+ search = after + 1;
1342
+ }
1343
+ return void 0;
1165
1344
  }
1166
- function takeSlackInlinePrefix(text, options) {
1167
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1168
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1169
- const normalized = text.replace(/\r\n?/g, "\n");
1170
- if (!normalized) {
1171
- return { prefix: "", rest: "" };
1345
+ function readExistingSlackAngleToken(line, start) {
1346
+ if (line[start] !== "<") {
1347
+ return void 0;
1172
1348
  }
1173
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1174
- return { prefix: normalized, rest: "" };
1349
+ const close = line.indexOf(">", start + 1);
1350
+ if (close === -1) {
1351
+ return void 0;
1175
1352
  }
1176
- const lineBounded = splitByLineBudget(normalized, maxLines);
1177
- const cutIndex = findSplitIndex(lineBounded, maxChars);
1178
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1179
- if (prefix) {
1180
- return {
1181
- prefix,
1182
- rest: normalized.slice(prefix.length).trimStart()
1183
- };
1353
+ const body = line.slice(start + 1, close);
1354
+ if (/^(?:https?:\/\/|@|#|!)/.test(body)) {
1355
+ return { text: line.slice(start, close + 1), end: close + 1 };
1184
1356
  }
1185
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1186
- return {
1187
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1188
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1189
- };
1357
+ return void 0;
1190
1358
  }
1191
- function splitSlackReplyText(text, options) {
1192
- const normalized = normalizeSlackReplyMarkdown(text);
1193
- if (!normalized) {
1194
- return [];
1359
+ function readMarkdownLink(line, start) {
1360
+ if (line[start] !== "[") {
1361
+ return void 0;
1195
1362
  }
1196
- const chunks = [];
1197
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1198
- let remaining = normalized;
1199
- while (remaining) {
1200
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1201
- if (fitsFinalChunk) {
1202
- chunks.push(
1203
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1204
- );
1205
- break;
1206
- }
1207
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1208
- ...continuationBudget,
1209
- forceSplit: true
1210
- });
1211
- chunks.push(renderedPrefix);
1212
- remaining = rest;
1363
+ const labelEnd = line.indexOf("](", start + 1);
1364
+ if (labelEnd === -1) {
1365
+ return void 0;
1213
1366
  }
1214
- if (chunks.length === 2) {
1215
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1367
+ const destStart = labelEnd + 2;
1368
+ if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) {
1369
+ return void 0;
1216
1370
  }
1217
- return chunks;
1371
+ const closeParens = line.indexOf(")", destStart);
1372
+ if (closeParens === -1) {
1373
+ return void 0;
1374
+ }
1375
+ return { text: line.slice(start, closeParens + 1), end: closeParens + 1 };
1218
1376
  }
1219
- function getSlackContinuationBudget() {
1220
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1377
+ function hasUnmatchedClosingParen(text) {
1378
+ let balance = 0;
1379
+ for (const ch of text) {
1380
+ if (ch === "(") balance++;
1381
+ else if (ch === ")") balance--;
1382
+ }
1383
+ return balance < 0;
1221
1384
  }
1222
- function buildSlackOutputMessage(text, files) {
1223
- const normalized = normalizeSlackReplyMarkdown(text);
1224
- const fileCount = files?.length ?? 0;
1225
- if (!normalized) {
1226
- if (fileCount > 0) {
1227
- return {
1228
- raw: "",
1229
- files
1230
- };
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;
1231
1391
  }
1232
- throw new Error(
1233
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1234
- );
1392
+ end++;
1235
1393
  }
1236
- return {
1237
- markdown: normalized,
1238
- files
1394
+ if (end === start) {
1395
+ return void 0;
1396
+ }
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);
1239
1402
  };
1403
+ const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
1404
+ while (raw.length > 0 && shouldPeel()) {
1405
+ peel();
1406
+ }
1407
+ if (!/^https?:\/\/.+/.test(raw)) {
1408
+ return void 0;
1409
+ }
1410
+ return { url: raw, suffix, end };
1240
1411
  }
1241
- var slackOutputPolicy = {
1242
- maxInlineChars: MAX_INLINE_CHARS,
1243
- maxInlineLines: MAX_INLINE_LINES
1244
- };
1245
-
1246
- // src/chat/xml.ts
1247
- function escapeXml(value) {
1248
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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++;
1444
+ }
1445
+ return result;
1249
1446
  }
1250
-
1251
- // src/chat/prompt.ts
1252
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1253
- function getLoggedMarkdownFiles() {
1254
- const globalState = globalThis;
1255
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
1256
- return globalState.__juniorLoggedMarkdownFiles;
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));
1458
+ }
1459
+ return out.join("\n");
1257
1460
  }
1258
- function loadOptionalMarkdownFile(candidates, fileName) {
1259
- for (const resolved of candidates) {
1260
- try {
1261
- const raw = fs.readFileSync(resolved, "utf8").trim();
1262
- if (raw.length > 0) {
1263
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
1264
- const logKey = `${fileName}:${resolved}`;
1265
- if (!loggedMarkdownFiles.has(logKey)) {
1266
- loggedMarkdownFiles.add(logKey);
1267
- logInfo(
1268
- `${fileName.toLowerCase()}_loaded`,
1269
- {},
1270
- {
1271
- "file.path": resolved
1272
- },
1273
- `Loaded ${fileName}`
1274
- );
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("");
1275
1475
  }
1276
- return raw;
1277
1476
  }
1278
- } catch {
1477
+ inCodeBlock = !inCodeBlock;
1478
+ result.push(line);
1279
1479
  continue;
1280
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);
1281
1490
  }
1282
- return null;
1283
- }
1284
- function loadSoul() {
1285
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1286
- if (soul) {
1287
- return soul;
1288
- }
1289
- logWarn(
1290
- "soul_load_fallback",
1291
- {},
1292
- {
1293
- "app.file.candidates": soulPathCandidates()
1294
- },
1295
- "SOUL.md not found; using built-in default personality"
1296
- );
1297
- return DEFAULT_SOUL;
1491
+ return result.join("\n");
1298
1492
  }
1299
- function loadWorld() {
1300
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
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();
1301
1498
  }
1302
- var JUNIOR_PERSONALITY = (() => {
1303
- try {
1304
- return loadSoul();
1305
- } catch (error) {
1306
- logWarn(
1307
- "soul_load_failed",
1308
- {},
1309
- {
1310
- "exception.message": error instanceof Error ? error.message : String(error)
1311
- },
1312
- "Failed to load SOUL.md; using built-in default personality"
1313
- );
1314
- return DEFAULT_SOUL;
1315
- }
1316
- })();
1317
- var JUNIOR_WORLD = (() => {
1318
- try {
1319
- return loadWorld();
1320
- } catch (error) {
1321
- logWarn(
1322
- "world_load_failed",
1323
- {},
1324
- {
1325
- "exception.message": error instanceof Error ? error.message : String(error)
1326
- },
1327
- "Failed to load WORLD.md; omitting world prompt context"
1328
- );
1329
- return null;
1499
+ function normalizeSlackStatusText(text) {
1500
+ const trimmed = text.trim();
1501
+ if (!trimmed) {
1502
+ return "";
1330
1503
  }
1331
- })();
1332
- function workspaceSkillDir(skillName) {
1333
- return sandboxSkillDir(skillName);
1504
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
1334
1505
  }
1335
- function formatConfigurationValue(value) {
1336
- if (typeof value === "string") {
1337
- return escapeXml(value);
1338
- }
1339
- try {
1340
- return escapeXml(JSON.stringify(value));
1341
- } catch {
1342
- return escapeXml(String(value));
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;
1343
1514
  }
1515
+ return text.split("\n").length;
1344
1516
  }
1345
- function renderRequesterBlock(fields) {
1346
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
1347
- if (lines.length === 0) {
1348
- return null;
1349
- }
1350
- return ["<requester>", ...lines, "</requester>"];
1351
- }
1352
- function renderTag(tag, lines) {
1353
- return [`<${tag}>`, ...lines, `</${tag}>`];
1354
- }
1355
- function renderTagBlock(tag, content) {
1356
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
1357
- }
1358
- function formatSkillEntry(skill) {
1359
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
1360
- const lines = [];
1361
- lines.push(" <skill>");
1362
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
1363
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
1364
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
1365
- lines.push(" </skill>");
1366
- return lines;
1517
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1518
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
1367
1519
  }
1368
- function formatAvailableSkillsForPrompt(skills, invocation) {
1369
- const autoSelectable = skills.filter(
1370
- (s) => s.disableModelInvocation !== true
1371
- );
1372
- const invokedExplicitOnly = invocation ? skills.filter(
1373
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
1374
- ) : [];
1375
- const sections = [];
1376
- if (autoSelectable.length > 0) {
1377
- const available = [
1378
- "<available-skills>",
1379
- "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."
1380
- ];
1381
- for (const skill of autoSelectable) {
1382
- available.push(...formatSkillEntry(skill));
1383
- }
1384
- available.push("</available-skills>");
1385
- sections.push(available.join("\n"));
1520
+ function findSplitIndex(text, maxChars) {
1521
+ if (text.length <= maxChars) {
1522
+ return text.length;
1386
1523
  }
1387
- if (invokedExplicitOnly.length > 0) {
1388
- const userCallable = [
1389
- "<user-callable-skills>",
1390
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
1391
- ];
1392
- for (const skill of invokedExplicitOnly) {
1393
- userCallable.push(...formatSkillEntry(skill));
1394
- }
1395
- userCallable.push("</user-callable-skills>");
1396
- sections.push(userCallable.join("\n"));
1524
+ const bounded = text.slice(0, maxChars);
1525
+ const newlineIndex = bounded.lastIndexOf("\n");
1526
+ if (newlineIndex > 0) {
1527
+ return newlineIndex;
1397
1528
  }
1398
- return sections.length > 0 ? sections.join("\n") : null;
1529
+ const spaceIndex = bounded.lastIndexOf(" ");
1530
+ if (spaceIndex > 0) {
1531
+ return spaceIndex;
1532
+ }
1533
+ return maxChars;
1399
1534
  }
1400
- function formatActiveMcpCatalogsForPrompt(catalogs) {
1401
- if (catalogs.length === 0) {
1402
- return null;
1535
+ function splitByLineBudget(text, maxLines) {
1536
+ if (maxLines <= 0) {
1537
+ return "";
1403
1538
  }
1404
- const lines = [
1405
- "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`."
1406
- ];
1407
- for (const catalog of catalogs) {
1408
- lines.push(" <catalog>");
1409
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
1410
- lines.push(
1411
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
1412
- );
1413
- lines.push(" </catalog>");
1539
+ const lines = text.split("\n");
1540
+ if (lines.length <= maxLines) {
1541
+ return text;
1414
1542
  }
1415
- return lines.join("\n");
1543
+ return lines.slice(0, maxLines).join("\n");
1416
1544
  }
1417
- function formatToolGuidanceForPrompt(tools) {
1418
- const guidedTools = tools.filter(
1419
- (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
1420
- );
1421
- if (guidedTools.length === 0) {
1422
- return null;
1423
- }
1424
- const lines = [];
1425
- for (const tool of guidedTools) {
1426
- lines.push(` <tool name="${escapeXml(tool.name)}">`);
1427
- if (tool.promptSnippet?.trim()) {
1428
- lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
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;
1429
1565
  }
1430
- if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1431
- for (const guideline of tool.promptGuidelines) {
1432
- lines.push(` - ${escapeXml(guideline)}`);
1433
- }
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;
1434
1575
  }
1435
- lines.push(" </tool>");
1436
1576
  }
1437
- return lines.join("\n");
1438
- }
1439
- function formatReferenceFilesLines() {
1440
- const files = listReferenceFiles();
1441
- if (files.length === 0) {
1577
+ if (!open) {
1442
1578
  return null;
1443
1579
  }
1444
- return files.map((filePath) => {
1445
- const name = path.basename(filePath);
1446
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1447
- });
1580
+ return {
1581
+ closeSuffix: text.endsWith("\n") ? open.fence : `
1582
+ ${open.fence}`,
1583
+ reopenPrefix: `${open.openerLine}
1584
+ `
1585
+ };
1448
1586
  }
1449
- function formatArtifactsLines(artifactState) {
1450
- if (!artifactState) return null;
1451
- const lines = [];
1452
- if (artifactState.lastCanvasId) {
1453
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
1587
+ function appendSlackSuffix(text, marker) {
1588
+ const carryover = getFenceContinuation(text);
1589
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1590
+ }
1591
+ function stripTrailingContinuationMarker(text) {
1592
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1593
+ }
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
+ ));
1454
1601
  }
1455
- if (artifactState.lastCanvasUrl) {
1456
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1602
+ let carryover = rest ? getFenceContinuation(prefix) : null;
1603
+ if (!carryover) {
1604
+ return { prefix, rest };
1457
1605
  }
1458
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1459
- lines.push("- recent_canvases:");
1460
- for (const canvas of artifactState.recentCanvases) {
1461
- lines.push(` - id: ${escapeXml(canvas.id)}`);
1462
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1463
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1464
- if (canvas.createdAt) {
1465
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1466
- }
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
+ };
1624
+ }
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);
1467
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: "" };
1468
1649
  }
1469
- if (artifactState.lastListId) {
1470
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1650
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1651
+ return { prefix: normalized, rest: "" };
1471
1652
  }
1472
- if (artifactState.lastListUrl) {
1473
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
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
+ };
1474
1661
  }
1475
- return lines.length > 0 ? lines : null;
1476
- }
1477
- function formatConfigurationLines(configuration) {
1478
- const keys = Object.keys(configuration ?? {}).sort(
1479
- (a, b) => a.localeCompare(b)
1480
- );
1481
- if (keys.length === 0) return null;
1482
- return keys.map(
1483
- (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
1484
- );
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
+ };
1485
1667
  }
1486
- 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.";
1487
- var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1488
- var TOOL_POLICY_RULES = [
1489
- "- 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.",
1490
- "- 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.",
1491
- "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1492
- "- 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.",
1493
- "- 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.",
1494
- `- 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.`,
1495
- "- 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.",
1496
- "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1497
- "- 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."
1498
- ];
1499
- var TOOL_CALL_STYLE_RULES = [
1500
- "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1501
- "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1502
- "- 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.",
1503
- "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1504
- ];
1505
- var SKILL_POLICY_RULES = [
1506
- "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1507
- "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1508
- ];
1509
- var EXECUTION_CONTRACT_RULES = [
1510
- "- Actionable request: act in this turn.",
1511
- "- 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.",
1512
- "- 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.",
1513
- "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1514
- "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1515
- "- 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."
1516
- ];
1517
- var CONVERSATION_RULES = [
1518
- "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1519
- "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1520
- "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1521
- ];
1522
- var SLACK_ACTION_RULES = [
1523
- "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1524
- "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1525
- "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1526
- "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
1527
- "- 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.",
1528
- "- Do not use reactions as progress indicators."
1529
- ];
1530
- var SAFETY_RULES = [
1531
- "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1532
- "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1533
- "- 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."
1534
- ];
1535
- var FAILURE_RULES = [
1536
- "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1537
- "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
1538
- "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
1539
- ];
1540
- function renderRuleSection(tag, lines) {
1541
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
1542
- }
1543
- function buildBehaviorSection() {
1544
- return [
1545
- renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1546
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1547
- renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1548
- renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
1549
- renderRuleSection("conversation", CONVERSATION_RULES),
1550
- renderRuleSection("slack-actions", SLACK_ACTION_RULES),
1551
- renderRuleSection("safety", SAFETY_RULES),
1552
- renderRuleSection("failure-handling", FAILURE_RULES)
1553
- ].join("\n\n");
1554
- }
1555
- function buildOutputSection() {
1556
- const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
1557
- return [
1558
- openTag,
1559
- "- Start with the answer or result, not internal process narration.",
1560
- "- 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.",
1561
- "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
1562
- "- 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.",
1563
- "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
1564
- "</output>"
1565
- ].join("\n");
1566
- }
1567
- function buildIdentitySection() {
1568
- return [
1569
- "# Identity",
1570
- `Your Slack username is \`${botConfig.userName}\`.`
1571
- ].join("\n");
1572
- }
1573
- function buildPersonalitySection() {
1574
- return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
1575
- }
1576
- function buildWorldSection() {
1577
- if (!JUNIOR_WORLD) {
1578
- return null;
1579
- }
1580
- return ["# World", JUNIOR_WORLD.trim()].join("\n");
1581
- }
1582
- function buildRuntimeSection(params) {
1583
- const lines = [
1584
- params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
1585
- params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
1586
- params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
1587
- ].filter(Boolean);
1588
- if (lines.length === 0) {
1589
- return null;
1590
- }
1591
- return renderTagBlock("runtime", lines.join("\n"));
1592
- }
1593
- function buildContextSection(params) {
1594
- const blocks = [];
1595
- const referenceLines = formatReferenceFilesLines();
1596
- if (referenceLines) {
1597
- blocks.push(
1598
- renderTag("reference-files", [
1599
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
1600
- ...referenceLines
1601
- ])
1602
- );
1603
- }
1604
- const requesterLines = renderRequesterBlock({
1605
- full_name: params.requester?.fullName,
1606
- user_name: params.requester?.userName,
1607
- user_id: params.requester?.userId
1608
- });
1609
- if (requesterLines) {
1610
- blocks.push(requesterLines);
1611
- }
1612
- const artifactLines = formatArtifactsLines(params.artifactState);
1613
- if (artifactLines) {
1614
- blocks.push(renderTag("artifacts", artifactLines));
1615
- }
1616
- const configLines = formatConfigurationLines(params.configuration);
1617
- if (configLines) {
1618
- blocks.push(
1619
- renderTag("configuration", [
1620
- "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.",
1621
- ...configLines
1622
- ])
1623
- );
1624
- }
1625
- if (params.invocation) {
1626
- blocks.push(
1627
- renderTag("explicit-skill-trigger", [
1628
- "Treat this skill as selected. Load it unless the tool says it is unavailable.",
1629
- `/${escapeXml(params.invocation.skillName)}`
1630
- ])
1631
- );
1632
- }
1633
- const body = blocks.map((block) => block.join("\n")).join("\n\n");
1634
- if (!body) {
1635
- return null;
1636
- }
1637
- return renderTagBlock("context", body);
1638
- }
1639
- function buildCapabilitiesSection(params) {
1640
- const blocks = [];
1641
- const availableSkills = formatAvailableSkillsForPrompt(
1642
- params.availableSkills,
1643
- params.invocation
1644
- );
1645
- if (availableSkills) {
1646
- blocks.push(availableSkills);
1647
- }
1648
- const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1649
- params.activeMcpCatalogs
1650
- );
1651
- if (activeCatalogs) {
1652
- blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
1653
- }
1654
- const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
1655
- if (toolGuidance) {
1656
- blocks.push(renderTagBlock("tool-guidance", toolGuidance));
1657
- }
1658
- if (blocks.length === 0) {
1659
- return null;
1668
+ function splitSlackReplyText(text, options) {
1669
+ const normalized = normalizeSlackReplyMarkdown(text);
1670
+ if (!normalized) {
1671
+ return [];
1660
1672
  }
1661
- return blocks.join("\n\n");
1662
- }
1663
- var STATIC_SYSTEM_PROMPT = [
1664
- HEADER,
1665
- buildIdentitySection(),
1666
- buildPersonalitySection(),
1667
- buildWorldSection(),
1668
- buildBehaviorSection(),
1669
- buildOutputSection()
1670
- ].filter((section) => Boolean(section)).join("\n\n");
1671
- function buildSystemPrompt() {
1672
- return STATIC_SYSTEM_PROMPT;
1673
- }
1674
- function buildTurnContextPrompt(params) {
1675
- const includeSessionContext = params.includeSessionContext ?? true;
1676
- if (!includeSessionContext) {
1677
- return null;
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;
1678
1690
  }
1679
- const runtimeSections = [
1680
- buildCapabilitiesSection({
1681
- availableSkills: params.availableSkills,
1682
- activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1683
- invocation: params.invocation,
1684
- toolGuidance: params.toolGuidance ?? []
1685
- }),
1686
- buildContextSection({
1687
- requester: params.requester,
1688
- artifactState: params.artifactState,
1689
- configuration: params.configuration,
1690
- invocation: params.invocation
1691
- }),
1692
- buildRuntimeSection(params.runtime ?? {})
1693
- ].filter((section) => Boolean(section));
1694
- if (runtimeSections.length === 0) {
1695
- return null;
1691
+ if (chunks.length === 2) {
1692
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1696
1693
  }
1697
- const sections = [
1698
- `<${TURN_CONTEXT_TAG}>`,
1699
- TURN_CONTEXT_HEADER,
1700
- "The current user instruction appears after this block in the same message.",
1701
- ...runtimeSections,
1702
- `</${TURN_CONTEXT_TAG}>`
1703
- ].filter((section) => Boolean(section));
1704
- return sections.join("\n\n");
1705
- }
1706
-
1707
- // src/chat/state/turn-session.ts
1708
- import { THREAD_STATE_TTL_MS } from "chat";
1709
-
1710
- // src/chat/state/session-log.ts
1711
- import { isDeepStrictEqual } from "util";
1712
- import { z } from "zod";
1713
- var AGENT_SESSION_LOG_PREFIX = "junior:agent-session-log";
1714
- var AGENT_SESSION_LOG_SCHEMA_VERSION = 1;
1715
- var INITIAL_SESSION_ID = "session_0";
1716
- var SESSION_ID_PREFIX = "session_";
1717
- var piMessageSchema = z.object({
1718
- role: z.string()
1719
- }).passthrough().transform((value) => value);
1720
- var piMessageEntrySchema = z.object({
1721
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1722
- type: z.literal("pi_message"),
1723
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1724
- message: piMessageSchema
1725
- });
1726
- var projectionResetEntrySchema = z.object({
1727
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1728
- type: z.literal("projection_reset"),
1729
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1730
- messages: z.array(piMessageSchema)
1731
- });
1732
- var mcpProviderConnectedEntrySchema = z.object({
1733
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1734
- type: z.literal("mcp_provider_connected"),
1735
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1736
- provider: z.string().min(1)
1737
- });
1738
- var authorizationKindSchema = z.union([
1739
- z.literal("plugin"),
1740
- z.literal("mcp")
1741
- ]);
1742
- var authorizationRequestedEntrySchema = z.object({
1743
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1744
- type: z.literal("authorization_requested"),
1745
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
1746
- createdAtMs: z.number().int().nonnegative(),
1747
- kind: authorizationKindSchema,
1748
- provider: z.string().min(1),
1749
- requesterId: z.string().min(1),
1750
- authorizationId: z.string().min(1),
1751
- delivery: z.union([
1752
- z.literal("private_link_sent"),
1753
- z.literal("private_link_reused")
1754
- ])
1755
- });
1756
- var authorizationCompletedEntrySchema = z.object({
1757
- schemaVersion: z.literal(AGENT_SESSION_LOG_SCHEMA_VERSION),
1758
- type: z.literal("authorization_completed"),
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
- });
1766
- var sessionLogEntrySchema = z.discriminatedUnion("type", [
1767
- piMessageEntrySchema,
1768
- projectionResetEntrySchema,
1769
- mcpProviderConnectedEntrySchema,
1770
- authorizationRequestedEntrySchema,
1771
- authorizationCompletedEntrySchema
1772
- ]);
1773
- function key(scope) {
1774
- const prefix = getChatConfig().state.keyPrefix;
1775
- return [
1776
- ...prefix ? [prefix] : [],
1777
- AGENT_SESSION_LOG_PREFIX,
1778
- scope.conversationId
1779
- ].join(":");
1780
- }
1781
- function rawKey(scope) {
1782
- return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
1694
+ return chunks;
1783
1695
  }
1784
- function normalizeMessageCount(value) {
1785
- return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
1696
+ function getSlackContinuationBudget() {
1697
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1786
1698
  }
1787
- function countMatchingPrefix(left, right) {
1788
- const limit = Math.min(left.length, right.length);
1789
- for (let index = 0; index < limit; index += 1) {
1790
- if (!isDeepStrictEqual(left[index], right[index])) {
1791
- return index;
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
+ };
1792
1708
  }
1709
+ throw new Error(
1710
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1711
+ );
1793
1712
  }
1794
- return limit;
1713
+ return {
1714
+ markdown: normalized,
1715
+ files
1716
+ };
1795
1717
  }
1796
- function entrySessionId(entry) {
1797
- return entry.sessionId ?? INITIAL_SESSION_ID;
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;
1798
1729
  }
1799
- function latestProjectionResetIndex(entries) {
1800
- for (let index = entries.length - 1; index >= 0; index -= 1) {
1801
- if (entries[index]?.type === "projection_reset") {
1802
- return index;
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;
1803
1752
  }
1804
1753
  }
1805
- return -1;
1754
+ return null;
1806
1755
  }
1807
- function currentSessionId(entries) {
1808
- const resetIndex = latestProjectionResetIndex(entries);
1809
- if (resetIndex < 0) {
1810
- return INITIAL_SESSION_ID;
1756
+ function loadSoul() {
1757
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1758
+ if (soul) {
1759
+ return soul;
1811
1760
  }
1812
- 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;
1813
1770
  }
1814
- function nextSessionId(entries) {
1815
- const resetCount = entries.filter(
1816
- (entry) => entry.type === "projection_reset"
1817
- ).length;
1818
- return `${SESSION_ID_PREFIX}${resetCount + 1}`;
1771
+ function loadWorld() {
1772
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
1819
1773
  }
1820
- function projectionEntries(entries, sessionId) {
1821
- if (sessionId) {
1822
- const sessionEntries = [];
1823
- let started = false;
1824
- for (const entry of entries) {
1825
- const entryId = entrySessionId(entry);
1826
- if (!started) {
1827
- if (entryId !== sessionId) {
1828
- continue;
1829
- }
1830
- started = true;
1831
- } else if (entry.type === "projection_reset" && entryId !== sessionId) {
1832
- break;
1833
- }
1834
- if (entryId === sessionId) {
1835
- sessionEntries.push(entry);
1836
- }
1837
- }
1838
- 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;
1839
1787
  }
1840
- const resetIndex = latestProjectionResetIndex(entries);
1841
- const startIndex = resetIndex < 0 ? 0 : resetIndex;
1842
- const currentId = resetIndex < 0 ? INITIAL_SESSION_ID : entrySessionId(entries[resetIndex]);
1843
- return entries.slice(startIndex).filter((entry) => entrySessionId(entry) === currentId);
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;
1802
+ }
1803
+ })();
1804
+ function workspaceSkillDir(skillName) {
1805
+ return sandboxSkillDir(skillName);
1844
1806
  }
1845
- function piEntry(message, sessionId) {
1846
- return {
1847
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1848
- type: "pi_message",
1849
- sessionId,
1850
- message
1851
- };
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
+ }
1852
1816
  }
1853
- function resetEntry(messages, sessionId) {
1854
- return {
1855
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1856
- type: "projection_reset",
1857
- sessionId,
1858
- messages
1859
- };
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>"];
1860
1823
  }
1861
- function mcpProviderConnectedEntry(provider, sessionId) {
1862
- return {
1863
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1864
- type: "mcp_provider_connected",
1865
- sessionId,
1866
- provider
1867
- };
1824
+ function renderTag(tag, lines) {
1825
+ return [`<${tag}>`, ...lines, `</${tag}>`];
1868
1826
  }
1869
- function authorizationObservationMessage(entry) {
1870
- const label = entry.kind === "mcp" ? "MCP authorization" : "Authorization";
1871
- return {
1872
- role: "user",
1873
- content: [
1874
- {
1875
- type: "text",
1876
- text: `${label} completed for provider "${entry.provider}". Continue the blocked request and retry the provider operation if needed.`
1877
- }
1878
- ],
1879
- timestamp: entry.createdAtMs
1880
- };
1827
+ function renderTagBlock(tag, content) {
1828
+ return [`<${tag}>`, content, `</${tag}>`].join("\n");
1881
1829
  }
1882
- function authorizationRequestedEntry(args) {
1883
- return {
1884
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1885
- type: "authorization_requested",
1886
- sessionId: args.sessionId,
1887
- createdAtMs: args.createdAtMs,
1888
- kind: args.kind,
1889
- provider: args.provider,
1890
- requesterId: args.requesterId,
1891
- authorizationId: args.authorizationId,
1892
- delivery: args.delivery
1893
- };
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;
1894
1839
  }
1895
- function authorizationCompletedEntry(args) {
1896
- return {
1897
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
1898
- type: "authorization_completed",
1899
- sessionId: args.sessionId,
1900
- createdAtMs: args.createdAtMs,
1901
- kind: args.kind,
1902
- provider: args.provider,
1903
- requesterId: args.requesterId,
1904
- authorizationId: args.authorizationId
1905
- };
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;
1906
1871
  }
1907
- function decode(value) {
1908
- if (typeof value === "string") {
1909
- return decode(JSON.parse(value));
1872
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
1873
+ if (catalogs.length === 0) {
1874
+ return null;
1910
1875
  }
1911
- const parsed = sessionLogEntrySchema.safeParse(value);
1912
- if (parsed.success) {
1913
- 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>");
1914
1886
  }
1915
- return piEntry(piMessageSchema.parse(value), INITIAL_SESSION_ID);
1887
+ return lines.join("\n");
1916
1888
  }
1917
- function project(entries, sessionId) {
1918
- let messages = [];
1919
- for (const entry of projectionEntries(entries, sessionId)) {
1920
- if (entry.type === "pi_message") {
1921
- messages.push(entry.message);
1922
- continue;
1923
- }
1924
- if (entry.type === "authorization_completed") {
1925
- messages.push(authorizationObservationMessage(entry));
1926
- 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())}`);
1927
1901
  }
1928
- if (entry.type === "mcp_provider_connected" || entry.type === "authorization_requested") {
1929
- continue;
1902
+ if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1903
+ for (const guideline of tool.promptGuidelines) {
1904
+ lines.push(` - ${escapeXml(guideline)}`);
1905
+ }
1930
1906
  }
1931
- messages = [...entry.messages];
1907
+ lines.push(" </tool>");
1932
1908
  }
1933
- return messages;
1909
+ return lines.join("\n");
1934
1910
  }
1935
- function connectedMcpProviders(entries, sessionId) {
1936
- const providers = /* @__PURE__ */ new Set();
1937
- for (const entry of projectionEntries(entries, sessionId)) {
1938
- if (entry.type === "mcp_provider_connected") {
1939
- providers.add(entry.provider);
1940
- }
1911
+ function formatReferenceFilesLines() {
1912
+ const files = listReferenceFiles();
1913
+ if (files.length === 0) {
1914
+ return null;
1941
1915
  }
1942
- return [...providers].sort((left, right) => left.localeCompare(right));
1916
+ return files.map((filePath) => {
1917
+ const name = path.basename(filePath);
1918
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1919
+ });
1943
1920
  }
1944
- function commitEntries(existingMessages, nextMessages, sessionId, entries) {
1945
- const matchingPrefix = countMatchingPrefix(existingMessages, nextMessages);
1946
- if (matchingPrefix === existingMessages.length) {
1947
- return nextMessages.slice(matchingPrefix).map((message) => piEntry(message, sessionId));
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)}`);
1948
1926
  }
1949
- return [resetEntry(nextMessages, nextSessionId(entries))];
1950
- }
1951
- function redisStore(redisStateAdapter) {
1952
- const client = redisStateAdapter.getClient();
1953
- return {
1954
- async append({ entries, scope, ttlMs }) {
1955
- const listKey = key(scope);
1956
- if (entries.length > 0) {
1957
- await client.rPush(
1958
- listKey,
1959
- entries.map((entry) => JSON.stringify(entry))
1960
- );
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)}`);
1961
1938
  }
1962
- await client.pExpire(listKey, Math.max(1, ttlMs));
1963
- },
1964
- async read(scope) {
1965
- const values = await client.lRange(key(scope), 0, -1);
1966
- return values.map(decode);
1967
1939
  }
1968
- };
1940
+ }
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;
1969
1948
  }
1970
- function stateStore() {
1971
- const stateAdapter = getStateAdapter();
1972
- return {
1973
- async append({ entries, scope, ttlMs }) {
1974
- const listKey = rawKey(scope);
1975
- for (const entry of entries) {
1976
- await stateAdapter.appendToList(listKey, entry, {
1977
- ttlMs: Math.max(1, ttlMs)
1978
- });
1979
- }
1980
- },
1981
- async read(scope) {
1982
- const values = await stateAdapter.getList(rawKey(scope));
1983
- return values.map(decode);
1984
- }
1985
- };
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
+ );
1986
1957
  }
1987
- async function defaultStore() {
1988
- const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
1989
- if (redisStateAdapter) {
1990
- return redisStore(redisStateAdapter);
1991
- }
1992
- await stateAdapter.connect();
1993
- return stateStore();
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");
1994
2015
  }
1995
- async function loadEntries(args) {
1996
- const store = args.store ?? await defaultStore();
1997
- return await store.read(args);
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)
2031
+ );
2032
+ }
2033
+ return sections.join("\n\n");
1998
2034
  }
1999
- async function loadMessages(args) {
2000
- const messageCount = normalizeMessageCount(args.messageCount);
2001
- if (messageCount === 0) {
2002
- return [];
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");
2003
2044
  }
2004
- const store = args.store ?? await defaultStore();
2005
- const messages = project(await store.read(args), args.sessionId);
2006
- return messages.length >= messageCount ? messages.slice(0, messageCount) : void 0;
2045
+ const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
2046
+ return [
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");
2007
2055
  }
2008
- async function loadProjection(args) {
2009
- const store = args.store ?? await defaultStore();
2010
- return project(await store.read(args), args.sessionId);
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");
2011
2059
  }
2012
- async function loadConnectedMcpProviders(args) {
2013
- return connectedMcpProviders(await loadEntries(args));
2060
+ function buildPersonalitySection() {
2061
+ return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
2014
2062
  }
2015
- async function recordMcpProviderConnected(args) {
2016
- const store = args.store ?? await defaultStore();
2017
- const entries = await store.read(args);
2018
- const sessionId = currentSessionId(entries);
2019
- if (connectedMcpProviders(entries).includes(args.provider)) {
2020
- return;
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
+ );
2119
+ }
2120
+ const body = blocks.map((block) => block.join("\n")).join("\n\n");
2121
+ if (!body) {
2122
+ return null;
2021
2123
  }
2022
- await store.append({
2023
- scope: args,
2024
- entries: [mcpProviderConnectedEntry(args.provider, sessionId)],
2025
- ttlMs: args.ttlMs
2026
- });
2124
+ return renderTagBlock("context", body);
2027
2125
  }
2028
- async function recordAuthorizationRequested(args) {
2029
- const store = args.store ?? await defaultStore();
2030
- const entries = await store.read(args);
2031
- const sessionId = currentSessionId(entries);
2032
- if (projectionEntries(entries).some(
2033
- (entry) => entry.type === "authorization_requested" && entry.authorizationId === args.authorizationId
2034
- )) {
2035
- 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);
2036
2134
  }
2037
- await store.append({
2038
- scope: args,
2039
- entries: [
2040
- authorizationRequestedEntry({
2041
- createdAtMs: Date.now(),
2042
- kind: args.kind,
2043
- sessionId,
2044
- provider: args.provider,
2045
- requesterId: args.requesterId,
2046
- authorizationId: args.authorizationId,
2047
- delivery: args.delivery
2048
- })
2049
- ],
2050
- ttlMs: args.ttlMs
2051
- });
2052
- }
2053
- async function recordAuthorizationCompleted(args) {
2054
- const store = args.store ?? await defaultStore();
2055
- const entries = await store.read(args);
2056
- const sessionId = currentSessionId(entries);
2057
- if (projectionEntries(entries).some(
2058
- (entry) => entry.type === "authorization_completed" && entry.authorizationId === args.authorizationId
2059
- )) {
2060
- return;
2135
+ const activeCatalogs = formatActiveMcpCatalogsForPrompt(
2136
+ params.activeMcpCatalogs
2137
+ );
2138
+ if (activeCatalogs) {
2139
+ blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
2061
2140
  }
2062
- await store.append({
2063
- scope: args,
2064
- entries: [
2065
- authorizationCompletedEntry({
2066
- createdAtMs: Date.now(),
2067
- kind: args.kind,
2068
- sessionId,
2069
- provider: args.provider,
2070
- requesterId: args.requesterId,
2071
- authorizationId: args.authorizationId
2072
- })
2073
- ],
2074
- ttlMs: args.ttlMs
2075
- });
2141
+ const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
2142
+ if (toolGuidance) {
2143
+ blocks.push(renderTagBlock("tool-guidance", toolGuidance));
2144
+ }
2145
+ if (blocks.length === 0) {
2146
+ return null;
2147
+ }
2148
+ return blocks.join("\n\n");
2076
2149
  }
2077
- async function commitMessages(args) {
2078
- const store = args.store ?? await defaultStore();
2079
- const entries = await store.read(args);
2080
- const existingMessages = project(entries);
2081
- const currentId = currentSessionId(entries);
2082
- const nextEntries = commitEntries(
2083
- existingMessages,
2084
- args.messages,
2085
- currentId,
2086
- entries
2087
- );
2088
- await store.append({
2089
- scope: args,
2090
- entries: nextEntries,
2091
- ttlMs: args.ttlMs
2092
- });
2093
- return {
2094
- sessionId: nextEntries.find((entry) => entry.type === "projection_reset")?.sessionId ?? currentId
2095
- };
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");
2096
2198
  }
2097
2199
 
2098
2200
  // src/chat/state/turn-session.ts
2201
+ import { THREAD_STATE_TTL_MS } from "chat";
2099
2202
  var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
2100
2203
  var AGENT_TURN_SESSION_INDEX_KEY = `${AGENT_TURN_SESSION_PREFIX}:index`;
2101
2204
  var AGENT_TURN_SESSION_INDEX_MAX_LENGTH = 5e3;
@@ -2109,6 +2212,9 @@ function agentTurnSessionConversationIndexKey(conversationId) {
2109
2212
  function toFiniteNonNegativeNumber(value) {
2110
2213
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
2111
2214
  }
2215
+ function toNonNegativeInteger(value) {
2216
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2217
+ }
2112
2218
  function parseAgentTurnUsage(value) {
2113
2219
  if (!isRecord(value)) {
2114
2220
  return void 0;
@@ -2128,23 +2234,6 @@ function parseAgentTurnUsage(value) {
2128
2234
  }
2129
2235
  return Object.keys(usage).length > 0 ? usage : void 0;
2130
2236
  }
2131
- function parseAgentTurnRequester(value) {
2132
- if (!isRecord(value)) {
2133
- return void 0;
2134
- }
2135
- const requester = {};
2136
- for (const field of [
2137
- "email",
2138
- "fullName",
2139
- "slackUserId",
2140
- "slackUserName"
2141
- ]) {
2142
- if (typeof value[field] === "string" && value[field].trim()) {
2143
- requester[field] = value[field].trim();
2144
- }
2145
- }
2146
- return Object.keys(requester).length > 0 ? requester : void 0;
2147
- }
2148
2237
  function parseStoredRecord(value) {
2149
2238
  if (isRecord(value)) {
2150
2239
  return value;
@@ -2184,9 +2273,12 @@ function parseAgentTurnSessionFields(parsed) {
2184
2273
  const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
2185
2274
  const lastProgressAtMs = toFiniteNonNegativeNumber(parsed.lastProgressAtMs);
2186
2275
  const logSessionId = typeof parsed.logSessionId === "string" ? parsed.logSessionId : void 0;
2187
- const requester = parseAgentTurnRequester(parsed.requester);
2276
+ const requester = parseStoredSlackRequester(parsed.requester);
2188
2277
  const startedAtMs = toFiniteNonNegativeNumber(parsed.startedAtMs);
2189
2278
  const surface = parseAgentTurnSurface(parsed.surface);
2279
+ const turnStartMessageIndex = toNonNegativeInteger(
2280
+ parsed.turnStartMessageIndex
2281
+ );
2190
2282
  const destination = parsed.destination === void 0 ? void 0 : parseDestination(parsed.destination);
2191
2283
  if (typeof conversationId !== "string" || typeof sessionId !== "string" || sliceId === void 0 || version === void 0 || updatedAtMs === void 0 || parsed.destination !== void 0 && !destination) {
2192
2284
  return void 0;
@@ -2215,6 +2307,7 @@ function parseAgentTurnSessionFields(parsed) {
2215
2307
  ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
2216
2308
  ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {},
2217
2309
  ...surface ? { surface } : {},
2310
+ ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {},
2218
2311
  ...typeof parsed.traceId === "string" ? { traceId: parsed.traceId } : {}
2219
2312
  };
2220
2313
  }
@@ -2247,6 +2340,7 @@ function parseAgentTurnSessionSummary(value) {
2247
2340
  const {
2248
2341
  errorMessage: _errorMessage,
2249
2342
  logSessionId: _logSessionId,
2343
+ turnStartMessageIndex: _turnStartMessageIndex,
2250
2344
  ...summary
2251
2345
  } = parsed;
2252
2346
  return summary;
@@ -2265,6 +2359,27 @@ async function appendAgentTurnSessionSummary(summary, ttlMs) {
2265
2359
  )
2266
2360
  ]);
2267
2361
  }
2362
+ async function recordConversationActivityBestEffort(args) {
2363
+ try {
2364
+ await recordConversationActivity({
2365
+ activityAtMs: args.summary.updatedAtMs,
2366
+ channelName: args.summary.channelName,
2367
+ conversationId: args.summary.conversationId,
2368
+ destination: args.summary.destination,
2369
+ nowMs: args.nowMs,
2370
+ requester: args.summary.requester,
2371
+ source: args.summary.surface
2372
+ });
2373
+ } catch (error) {
2374
+ logException(
2375
+ error,
2376
+ "conversation_activity_record_failed",
2377
+ { conversationId: args.summary.conversationId },
2378
+ {},
2379
+ "Failed to mirror turn session summary into conversation activity"
2380
+ );
2381
+ }
2382
+ }
2268
2383
  function materializePiMessages(committedMessageCount, includeProjectionTail, sessionMessages, sessionProjection) {
2269
2384
  if (committedMessageCount === 0) {
2270
2385
  return sessionProjection;
@@ -2301,7 +2416,8 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
2301
2416
  ...stored.requester ? { requester: stored.requester } : {},
2302
2417
  ...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
2303
2418
  ...stored.surface ? { surface: stored.surface } : {},
2304
- ...stored.traceId ? { traceId: stored.traceId } : {}
2419
+ ...stored.traceId ? { traceId: stored.traceId } : {},
2420
+ ...stored.turnStartMessageIndex !== void 0 ? { turnStartMessageIndex: stored.turnStartMessageIndex } : {}
2305
2421
  };
2306
2422
  }
2307
2423
  async function getStoredAgentTurnSessionRecord(conversationId, sessionId) {
@@ -2367,7 +2483,8 @@ function buildStoredRecord(args) {
2367
2483
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
2368
2484
  ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2369
2485
  ...args.surface ? { surface: args.surface } : {},
2370
- ...args.traceId ? { traceId: args.traceId } : {}
2486
+ ...args.traceId ? { traceId: args.traceId } : {},
2487
+ ...args.turnStartMessageIndex !== void 0 ? { turnStartMessageIndex: args.turnStartMessageIndex } : {}
2371
2488
  };
2372
2489
  }
2373
2490
  async function setStoredRecord(args) {
@@ -2382,6 +2499,7 @@ async function setStoredRecord(args) {
2382
2499
  committedMessageCount: _committedMessageCount,
2383
2500
  errorMessage: _errorMessage,
2384
2501
  logSessionId: _logSessionId,
2502
+ turnStartMessageIndex: _turnStartMessageIndex,
2385
2503
  ...summary
2386
2504
  } = args.record;
2387
2505
  await appendAgentTurnSessionSummary(summary, args.ttlMs);
@@ -2418,6 +2536,7 @@ async function updateAgentTurnSessionState(args) {
2418
2536
  ...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
2419
2537
  ...args.existing.surface ? { surface: args.existing.surface } : {},
2420
2538
  ...args.existing.traceId ? { traceId: args.existing.traceId } : {},
2539
+ ...args.existing.turnStartMessageIndex !== void 0 ? { turnStartMessageIndex: args.existing.turnStartMessageIndex } : {},
2421
2540
  ...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
2422
2541
  })
2423
2542
  });
@@ -2431,6 +2550,7 @@ async function upsertAgentTurnSessionRecord(args) {
2431
2550
  const commit = await commitMessages({
2432
2551
  conversationId: args.conversationId,
2433
2552
  messages: args.piMessages,
2553
+ requester: args.requester ?? existingRecord?.requester,
2434
2554
  ttlMs
2435
2555
  });
2436
2556
  return await setStoredRecord({
@@ -2456,7 +2576,10 @@ async function upsertAgentTurnSessionRecord(args) {
2456
2576
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
2457
2577
  ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2458
2578
  ...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
2459
- ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
2579
+ ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {},
2580
+ ...(args.turnStartMessageIndex ?? existingRecord?.turnStartMessageIndex) !== void 0 ? {
2581
+ turnStartMessageIndex: args.turnStartMessageIndex ?? existingRecord?.turnStartMessageIndex
2582
+ } : {}
2460
2583
  })
2461
2584
  });
2462
2585
  }
@@ -2467,32 +2590,34 @@ async function recordAgentTurnSessionSummary(args) {
2467
2590
  );
2468
2591
  const nowMs = Date.now();
2469
2592
  const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
2470
- await appendAgentTurnSessionSummary(
2471
- {
2472
- version: existing?.version ?? 0,
2473
- ...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
2474
- conversationId: args.conversationId,
2475
- sessionId: args.sessionId,
2476
- sliceId: args.sliceId,
2477
- startedAtMs: existing?.startedAtMs ?? args.startedAtMs ?? nowMs,
2478
- lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
2479
- state: args.state,
2480
- updatedAtMs: nowMs,
2481
- cumulativeDurationMs: toFiniteNonNegativeNumber(args.cumulativeDurationMs) ?? existing?.cumulativeDurationMs ?? 0,
2482
- ...args.cumulativeUsage ?? existing?.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage ?? existing?.cumulativeUsage } : {},
2483
- ...args.destination ?? existing?.destination ? { destination: args.destination ?? existing?.destination } : {},
2484
- ...args.requester ?? existing?.requester ? { requester: args.requester ?? existing?.requester } : {},
2485
- ...Array.isArray(args.loadedSkillNames) ? {
2486
- loadedSkillNames: args.loadedSkillNames.filter(
2487
- (value) => typeof value === "string"
2488
- )
2489
- } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
2490
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
2491
- ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
2492
- ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
2493
- },
2494
- ttlMs
2495
- );
2593
+ const summary = {
2594
+ version: existing?.version ?? 0,
2595
+ ...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
2596
+ conversationId: args.conversationId,
2597
+ sessionId: args.sessionId,
2598
+ sliceId: args.sliceId,
2599
+ startedAtMs: existing?.startedAtMs ?? args.startedAtMs ?? nowMs,
2600
+ lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
2601
+ state: args.state,
2602
+ updatedAtMs: nowMs,
2603
+ cumulativeDurationMs: toFiniteNonNegativeNumber(args.cumulativeDurationMs) ?? existing?.cumulativeDurationMs ?? 0,
2604
+ ...args.cumulativeUsage ?? existing?.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage ?? existing?.cumulativeUsage } : {},
2605
+ ...args.destination ?? existing?.destination ? { destination: args.destination ?? existing?.destination } : {},
2606
+ ...args.requester ?? existing?.requester ? { requester: args.requester ?? existing?.requester } : {},
2607
+ ...Array.isArray(args.loadedSkillNames) ? {
2608
+ loadedSkillNames: args.loadedSkillNames.filter(
2609
+ (value) => typeof value === "string"
2610
+ )
2611
+ } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
2612
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
2613
+ ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
2614
+ ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
2615
+ };
2616
+ await appendAgentTurnSessionSummary(summary, ttlMs);
2617
+ await recordConversationActivityBestEffort({
2618
+ nowMs,
2619
+ summary
2620
+ });
2496
2621
  }
2497
2622
  async function readAgentTurnSessionSummariesFromIndex(key2) {
2498
2623
  const stateAdapter = getStateAdapter();
@@ -2513,9 +2638,6 @@ async function readAgentTurnSessionSummariesFromIndex(key2) {
2513
2638
  (left, right) => right.updatedAtMs - left.updatedAtMs
2514
2639
  );
2515
2640
  }
2516
- async function listAgentTurnSessionSummaries(limit = 50) {
2517
- return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
2518
- }
2519
2641
  async function listAgentTurnSessionSummariesForConversation(conversationId) {
2520
2642
  const summaries = await readAgentTurnSessionSummariesFromIndex(
2521
2643
  agentTurnSessionConversationIndexKey(conversationId)
@@ -2554,249 +2676,19 @@ async function failAgentTurnSessionRecord(args) {
2554
2676
  });
2555
2677
  }
2556
2678
 
2557
- // src/chat/sentry-links.ts
2558
- function getSentryOrgSlug() {
2559
- const slug = process.env.SENTRY_ORG_SLUG?.trim();
2560
- return slug || void 0;
2561
- }
2562
- function isSentrySaasDsnHost(host) {
2563
- return host === "sentry.io" || host.endsWith(".sentry.io");
2564
- }
2565
- function buildSentryWebBaseUrl(dsn) {
2566
- if (isSentrySaasDsnHost(dsn.host)) {
2567
- return "https://sentry.io";
2568
- }
2569
- const port = dsn.port ? `:${dsn.port}` : "";
2570
- const path2 = dsn.path ? `/${dsn.path}` : "";
2571
- return `${dsn.protocol}://${dsn.host}${port}${path2}`;
2572
- }
2573
- function buildSentryConversationUrl(conversationId) {
2574
- const client = sentry_exports.getClient();
2575
- const dsn = client?.getDsn();
2576
- if (!dsn?.host || !dsn.projectId) {
2577
- return void 0;
2578
- }
2579
- const orgSlug = getSentryOrgSlug();
2580
- if (!orgSlug) {
2581
- return void 0;
2582
- }
2583
- const encodedId = encodeURIComponent(conversationId);
2584
- const params = new URLSearchParams();
2585
- params.set("project", dsn.projectId);
2586
- const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
2587
- if (isSentrySaasDsnHost(dsn.host)) {
2588
- return `https://${orgSlug}.sentry.io/${path2}`;
2589
- }
2590
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
2591
- }
2592
- function buildSentryTraceUrl(traceId) {
2593
- const client = sentry_exports.getClient();
2594
- const dsn = client?.getDsn();
2595
- if (!dsn?.host || !dsn.projectId) {
2596
- return void 0;
2597
- }
2598
- const orgSlug = getSentryOrgSlug();
2599
- if (!orgSlug) {
2600
- return void 0;
2601
- }
2602
- const encodedTraceId = encodeURIComponent(traceId);
2603
- const params = new URLSearchParams();
2604
- params.set("project", dsn.projectId);
2605
- const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
2606
- if (isSentrySaasDsnHost(dsn.host)) {
2607
- return `https://${orgSlug}.sentry.io/${path2}`;
2608
- }
2609
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
2610
- }
2611
-
2612
- // src/chat/slack/conversation-context.ts
2613
- function normalizeConversationName(type, channelName) {
2614
- const trimmed = channelName?.trim();
2615
- if (!trimmed) return void 0;
2616
- if (type === "public_channel" || type === "private_channel" || type === "private_channel_or_group_dm") {
2617
- return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
2618
- }
2619
- return trimmed;
2620
- }
2621
- function typeFromSlackChannelType(channelType) {
2622
- if (channelType === "channel") return "public_channel";
2623
- if (channelType === "group") return "private_channel";
2624
- if (channelType === "mpim") return "group_dm";
2625
- if (channelType === "im") return "direct_message";
2626
- return void 0;
2627
- }
2628
- function typeFromChannelId(channelId, channelName) {
2629
- const normalized = normalizeSlackConversationId(channelId);
2630
- if (!normalized) return void 0;
2631
- if (normalized.startsWith("C")) return "public_channel";
2632
- if (normalized.startsWith("D")) return "direct_message";
2633
- if (normalized.startsWith("G")) {
2634
- return channelName?.trim().startsWith("mpdm-") ? "group_dm" : "private_channel_or_group_dm";
2635
- }
2636
- return void 0;
2637
- }
2638
- function toSlackEventChannelType(channelType) {
2639
- if (channelType === "channel" || channelType === "group" || channelType === "mpim" || channelType === "im") {
2640
- return channelType;
2641
- }
2642
- return void 0;
2643
- }
2644
- function resolveSlackChannelTypeFromMessage(message) {
2645
- const raw = message.raw;
2646
- if (!raw || typeof raw !== "object") {
2647
- return void 0;
2648
- }
2649
- const channelType = raw.channel_type;
2650
- return typeof channelType === "string" ? toSlackEventChannelType(channelType.trim()) : void 0;
2651
- }
2652
- function resolveSlackConversationContext(input) {
2653
- const type = typeFromSlackChannelType(input.channelType) ?? typeFromChannelId(input.channelId, input.channelName);
2654
- if (!type) return void 0;
2655
- const name = normalizeConversationName(type, input.channelName);
2656
- return {
2657
- type,
2658
- ...name ? { name } : {}
2659
- };
2660
- }
2661
- function resolveSlackConversationContextFromThreadId(input) {
2662
- const slackThread = parseSlackThreadId(input.threadId);
2663
- return resolveSlackConversationContext({
2664
- channelId: slackThread?.channelId,
2665
- channelName: input.channelName
2666
- });
2667
- }
2668
- function formatSlackConversationTypeLabel(type) {
2669
- if (type === "public_channel") return "Public Channel";
2670
- if (type === "private_channel") return "Private Channel";
2671
- if (type === "group_dm") return "Group DM";
2672
- if (type === "direct_message") return "Direct Message";
2673
- return "Private Channel or Group DM";
2674
- }
2675
- function formatSlackConversationRedactedLabel(context) {
2676
- if (!context) return void 0;
2677
- return formatSlackConversationTypeLabel(context.type);
2678
- }
2679
-
2680
- // src/chat/state/conversation-details.ts
2681
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
2682
- var CONVERSATION_PREFIX = "junior:conversation";
2683
- var CONVERSATION_DETAILS_TTL_MS = THREAD_STATE_TTL_MS2;
2684
- function conversationContextKey(conversationId) {
2685
- return `${CONVERSATION_PREFIX}:${conversationId}:context`;
2686
- }
2687
- function conversationTitleKey(conversationId) {
2688
- return `${CONVERSATION_PREFIX}:${conversationId}:title`;
2689
- }
2690
- function parseAgentTurnRequester2(value) {
2691
- if (!isRecord(value)) return void 0;
2692
- const requester = {
2693
- ...typeof value.email === "string" ? { email: value.email } : {},
2694
- ...typeof value.fullName === "string" ? { fullName: value.fullName } : {},
2695
- ...typeof value.slackUserId === "string" ? { slackUserId: value.slackUserId } : {},
2696
- ...typeof value.slackUserName === "string" ? { slackUserName: value.slackUserName } : {}
2697
- };
2698
- return Object.keys(requester).length > 0 ? requester : void 0;
2699
- }
2700
- function parseOriginSurface(value) {
2701
- if (value === "slack" || value === "api" || value === "scheduler" || value === "internal") {
2702
- return value;
2703
- }
2704
- return void 0;
2705
- }
2706
- function storedContextFromInput(context) {
2707
- return {
2708
- ...context.channelName ? { channelName: context.channelName } : {},
2709
- ...context.originSurface ? { originSurface: context.originSurface } : {},
2710
- ...context.originRequester ? { originRequester: context.originRequester } : {},
2711
- startedAtMs: context.startedAtMs
2712
- };
2713
- }
2714
- function parseContext(value) {
2715
- if (!isRecord(value)) return void 0;
2716
- const startedAtMs = toOptionalNumber(value.startedAtMs);
2717
- if (startedAtMs === void 0) return void 0;
2718
- return {
2719
- ...typeof value.channelName === "string" && value.channelName.trim() ? { channelName: value.channelName.trim() } : {},
2720
- ...parseOriginSurface(value.originSurface) ? { originSurface: parseOriginSurface(value.originSurface) } : {},
2721
- ...parseAgentTurnRequester2(value.originRequester) ? { originRequester: parseAgentTurnRequester2(value.originRequester) } : {},
2722
- startedAtMs
2723
- };
2724
- }
2725
- function parseTitle(value) {
2726
- if (!isRecord(value)) return void 0;
2727
- const displayTitle = typeof value.displayTitle === "string" && value.displayTitle.trim() ? value.displayTitle.trim() : void 0;
2728
- if (!displayTitle) return void 0;
2729
- return {
2730
- displayTitle,
2731
- ...typeof value.titleSourceMessageId === "string" ? { titleSourceMessageId: value.titleSourceMessageId } : {}
2732
- };
2733
- }
2734
- async function initConversationContext(conversationId, context) {
2735
- const stateAdapter = getStateAdapter();
2736
- await stateAdapter.connect();
2737
- const key2 = conversationContextKey(conversationId);
2738
- const inserted = await stateAdapter.setIfNotExists(
2739
- key2,
2740
- storedContextFromInput(context),
2741
- CONVERSATION_DETAILS_TTL_MS
2742
- );
2743
- if (inserted) return;
2744
- const existing = parseContext(await stateAdapter.get(key2));
2745
- if (!existing) {
2746
- return;
2747
- }
2748
- await stateAdapter.set(key2, existing, CONVERSATION_DETAILS_TTL_MS);
2749
- }
2750
- async function setConversationTitle(conversationId, title) {
2751
- const stateAdapter = getStateAdapter();
2752
- await stateAdapter.connect();
2753
- await stateAdapter.set(
2754
- conversationTitleKey(conversationId),
2755
- {
2756
- displayTitle: title.displayTitle,
2757
- ...title.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {}
2758
- },
2759
- CONVERSATION_DETAILS_TTL_MS
2760
- );
2761
- }
2762
- async function getConversationDetails(conversationId) {
2763
- const stateAdapter = getStateAdapter();
2764
- await stateAdapter.connect();
2765
- const [rawContext, rawTitle] = await Promise.all([
2766
- stateAdapter.get(conversationContextKey(conversationId)),
2767
- stateAdapter.get(conversationTitleKey(conversationId))
2768
- ]);
2769
- const context = parseContext(rawContext);
2770
- const title = parseTitle(rawTitle);
2771
- if (!context && !title) return void 0;
2772
- return {
2773
- conversationId,
2774
- ...title?.displayTitle ? { displayTitle: title.displayTitle } : {},
2775
- ...title?.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {},
2776
- ...context?.channelName ? { channelName: context.channelName } : {},
2777
- ...context?.originSurface ? { originSurface: context.originSurface } : {},
2778
- ...context?.originRequester ? { originRequester: context.originRequester } : {},
2779
- ...context?.startedAtMs !== void 0 ? { startedAtMs: context.startedAtMs } : {}
2780
- };
2781
- }
2782
- async function getConversationDetailsForIds(conversationIds) {
2783
- const uniqueIds = [...new Set(conversationIds)].filter(Boolean);
2784
- const entries = await Promise.all(
2785
- uniqueIds.map(async (id) => {
2786
- const details = await getConversationDetails(id);
2787
- return details ? [id, details] : void 0;
2788
- })
2789
- );
2790
- const result = /* @__PURE__ */ new Map();
2791
- for (const entry of entries) {
2792
- if (entry) result.set(entry[0], entry[1]);
2793
- }
2794
- return result;
2795
- }
2796
-
2797
2679
  export {
2680
+ getInterruptionMarker,
2681
+ truncateStatusText,
2682
+ normalizeSlackStatusText,
2683
+ splitSlackReplyText,
2684
+ buildSlackOutputMessage,
2685
+ escapeXml,
2686
+ JUNIOR_PERSONALITY,
2687
+ buildSystemPrompt,
2688
+ buildTurnContextPrompt,
2798
2689
  createAgentPluginLogger,
2799
2690
  createPluginState,
2691
+ getSlackToolContext,
2800
2692
  bindSlackDirectCredentialSubject,
2801
2693
  verifySlackDirectCredentialSubject,
2802
2694
  resolveChannelCapabilities,
@@ -2808,16 +2700,6 @@ export {
2808
2700
  getAgentPluginSlackConversationLink,
2809
2701
  getAgentPluginOperationalReports,
2810
2702
  createAgentPluginHookRunner,
2811
- GET,
2812
- getInterruptionMarker,
2813
- truncateStatusText,
2814
- normalizeSlackStatusText,
2815
- splitSlackReplyText,
2816
- buildSlackOutputMessage,
2817
- escapeXml,
2818
- JUNIOR_PERSONALITY,
2819
- buildSystemPrompt,
2820
- buildTurnContextPrompt,
2821
2703
  loadProjection,
2822
2704
  loadConnectedMcpProviders,
2823
2705
  recordMcpProviderConnected,
@@ -2827,18 +2709,7 @@ export {
2827
2709
  getAgentTurnSessionRecord,
2828
2710
  upsertAgentTurnSessionRecord,
2829
2711
  recordAgentTurnSessionSummary,
2830
- listAgentTurnSessionSummaries,
2831
2712
  listAgentTurnSessionSummariesForConversation,
2832
2713
  abandonAgentTurnSessionRecord,
2833
- failAgentTurnSessionRecord,
2834
- buildSentryConversationUrl,
2835
- buildSentryTraceUrl,
2836
- resolveSlackChannelTypeFromMessage,
2837
- resolveSlackConversationContext,
2838
- resolveSlackConversationContextFromThreadId,
2839
- formatSlackConversationRedactedLabel,
2840
- initConversationContext,
2841
- setConversationTitle,
2842
- getConversationDetails,
2843
- getConversationDetailsForIds
2714
+ failAgentTurnSessionRecord
2844
2715
  };