@diegoaltoworks/chatter 0.67.0 → 2.0.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 (72) hide show
  1. package/README.md +3 -1
  2. package/dist/channels/gates.d.ts +21 -2
  3. package/dist/channels/gates.d.ts.map +1 -1
  4. package/dist/channels/index.d.ts +5 -3
  5. package/dist/channels/index.d.ts.map +1 -1
  6. package/dist/channels/index.js +31 -13
  7. package/dist/channels/index.mjs +30 -13
  8. package/dist/channels/matrix/channel.d.ts +2 -0
  9. package/dist/channels/matrix/channel.d.ts.map +1 -1
  10. package/dist/channels/matrix/handler.d.ts +9 -0
  11. package/dist/channels/matrix/handler.d.ts.map +1 -1
  12. package/dist/channels/matrix/index.d.ts +1 -1
  13. package/dist/channels/matrix/index.d.ts.map +1 -1
  14. package/dist/channels/matrix/index.js +75 -20
  15. package/dist/channels/matrix/index.mjs +74 -20
  16. package/dist/channels/matrix/updates.d.ts +8 -2
  17. package/dist/channels/matrix/updates.d.ts.map +1 -1
  18. package/dist/channels/pipeline.d.ts +34 -4
  19. package/dist/channels/pipeline.d.ts.map +1 -1
  20. package/dist/channels/polling.d.ts +11 -0
  21. package/dist/channels/polling.d.ts.map +1 -1
  22. package/dist/channels/telegram/channel.d.ts +2 -0
  23. package/dist/channels/telegram/channel.d.ts.map +1 -1
  24. package/dist/channels/telegram/handler.d.ts +9 -0
  25. package/dist/channels/telegram/handler.d.ts.map +1 -1
  26. package/dist/channels/telegram/index.d.ts +1 -1
  27. package/dist/channels/telegram/index.d.ts.map +1 -1
  28. package/dist/channels/telegram/index.js +73 -24
  29. package/dist/channels/telegram/index.mjs +72 -24
  30. package/dist/channels/telegram/updates.d.ts +8 -2
  31. package/dist/channels/telegram/updates.d.ts.map +1 -1
  32. package/dist/channels/telegram/webhook.d.ts +10 -0
  33. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  34. package/dist/channels/whatsapp/channel.d.ts +8 -0
  35. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  36. package/dist/channels/whatsapp/inbound.d.ts +2 -0
  37. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  38. package/dist/channels/whatsapp/index.js +45 -18
  39. package/dist/channels/whatsapp/index.mjs +45 -18
  40. package/dist/core/answer.d.ts +9 -2
  41. package/dist/core/answer.d.ts.map +1 -1
  42. package/dist/core/guardrails.d.ts +11 -1
  43. package/dist/core/guardrails.d.ts.map +1 -1
  44. package/dist/core/llm.d.ts +3 -1
  45. package/dist/core/llm.d.ts.map +1 -1
  46. package/dist/history/index.js +12 -9
  47. package/dist/history/index.mjs +12 -9
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +50 -27
  51. package/dist/index.mjs +49 -27
  52. package/dist/mcp-server.d.ts.map +1 -1
  53. package/dist/mcp-server.js +16 -11
  54. package/dist/mcp-server.mjs +16 -11
  55. package/dist/personas/index.d.ts +1 -1
  56. package/dist/personas/index.d.ts.map +1 -1
  57. package/dist/personas/index.js +17 -5
  58. package/dist/personas/index.mjs +17 -5
  59. package/dist/personas/resolver.d.ts +35 -8
  60. package/dist/personas/resolver.d.ts.map +1 -1
  61. package/dist/routes/demo.d.ts.map +1 -1
  62. package/dist/routes/openai.d.ts.map +1 -1
  63. package/dist/routes/private.d.ts.map +1 -1
  64. package/dist/routes/public.d.ts.map +1 -1
  65. package/dist/scheduler/index.js +12 -9
  66. package/dist/scheduler/index.mjs +12 -9
  67. package/dist/server.d.ts.map +1 -1
  68. package/dist/server.js +44 -25
  69. package/dist/server.mjs +44 -25
  70. package/dist/types.d.ts +32 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +11 -11
package/README.md CHANGED
@@ -116,11 +116,13 @@ Complete guides for setup, deployment, and integration - see
116
116
  [0001, brain and sockets split](./docs/decisions/0001-brain-and-sockets-split.md),
117
117
  [0002, no LangChain in core](./docs/decisions/0002-no-langchain-in-core.md),
118
118
  [0003, slot-filling is brain territory](./docs/decisions/0003-slot-filling-is-brain-territory.md),
119
- [0004, main-protection keeps only non-fast-forward and deletion protections](./docs/decisions/0004-main-protection-stays-non-fast-forward-only.md)
119
+ [0004, main-protection keeps only non-fast-forward and deletion protections](./docs/decisions/0004-main-protection-stays-non-fast-forward-only.md),
120
+ [0005, chatter ships no content](./docs/decisions/0005-ships-no-content.md)
120
121
  - **Patterns** - Worked "how do I add X" guides:
121
122
  [adding a capability](./docs/patterns/adding-a-capability.md),
122
123
  [adding a store](./docs/patterns/adding-a-store.md),
123
124
  [adding a retriever](./docs/patterns/adding-a-retriever.md),
125
+ [extending chatter](./docs/patterns/extending-chatter.md),
124
126
  [exemplars](./docs/patterns/exemplars.md)
125
127
 
126
128
  ### Archive
@@ -29,6 +29,17 @@ export interface ChannelMessage {
29
29
  * messages a transport can't target individually) simply have none.
30
30
  */
31
31
  messageRef?: unknown;
32
+ /**
33
+ * Which of the bot's own endpoints received this message - the
34
+ * host-chosen key it was configured with (a WhatsApp `sessionId`, a
35
+ * Telegram/Matrix channel `name`), NOT a wire identity. It is stable
36
+ * across a token or SIM change and is already the key of
37
+ * {@link SessionIdentityRegistry}, so a caller binding behaviour to "which
38
+ * endpoint was reached" and the loop guard share one map rather than
39
+ * running two parallel mechanisms. A transport hosting only one endpoint,
40
+ * or one that hasn't chosen to populate it, leaves it unset.
41
+ */
42
+ endpointId?: string;
32
43
  }
33
44
  export type ChannelAction = "reply" | "ignore" | "mute" | "unmute";
34
45
  export interface ChannelGateConfig {
@@ -38,8 +49,9 @@ export interface ChannelGateConfig {
38
49
  mutedChats: Set<string>;
39
50
  /**
40
51
  * Caller-supplied trigger for "go quiet in this group chat". No default:
41
- * this module ships no bot name or in-character phrasing, so mute/unmute
42
- * are inert unless a host configures a pattern. Never applied to DMs.
52
+ * this module ships no content (see docs/decisions/0005-ships-no-content.md),
53
+ * so mute/unmute are inert unless a host configures a pattern. Never
54
+ * applied to DMs.
43
55
  */
44
56
  muteRegex?: RegExp;
45
57
  unmuteRegex?: RegExp;
@@ -115,6 +127,13 @@ export declare function underReplyRateLimit(msg: Pick<ChannelMessage, "chatId" |
115
127
  * session's own identity is still "us" while it reconnects, and losing that
116
128
  * during exactly the disconnect/reconnect window this guard exists for
117
129
  * would defeat it.
130
+ *
131
+ * One key space, shared by every endpoint writing into the same registry -
132
+ * `createServer` gives all its channels one (`ServerDependencies.identities`),
133
+ * where the built-ins key by WhatsApp session id and by channel name
134
+ * respectively. Two endpoints choosing the same key overwrite each other's
135
+ * identities silently, so a key must be unique across everything the server
136
+ * runs, not just within one transport.
118
137
  */
119
138
  export type SessionIdentityRegistry = Map<string, string[]>;
120
139
  /** True when `id` matches an identity of ANY session ever registered, not just the one that received the message. */
@@ -1 +1 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/channels/gates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,YAAY,EAAE,OAAO,CAAC;IACtB,sHAAsH;IACtH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kIAAkI;IAClI,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAC9C,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAUjG;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB;UAcR,MAAM,GAAG,OAAO;;EAiBrC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,iBAAiB,CAAC,EACvD,QAAQ,EAAE;IAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAChF,OAAO,CAET;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAE5D,qHAAqH;AACrH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,UAAU,CAAC,EACjD,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAET"}
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/channels/gates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,YAAY,EAAE,OAAO,CAAC;IACtB,sHAAsH;IACtH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kIAAkI;IAClI,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAC9C,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAUjG;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB;UAcR,MAAM,GAAG,OAAO;;EAiBrC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,iBAAiB,CAAC,EACvD,QAAQ,EAAE;IAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAChF,OAAO,CAET;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAE5D,qHAAqH;AACrH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,UAAU,CAAC,EACjD,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAET"}
@@ -4,8 +4,10 @@
4
4
  * configured channel with the same {@link ServerDependencies} route
5
5
  * factories and custom routes receive, so a channel can call
6
6
  * `prepareChat`/`answerOnce`, share `deps.db`, etc. without knowing how the
7
- * server was assembled — and `channel.start(deps)` works identically when
8
- * called directly, outside `createServer`, for standalone use.
7
+ * server was assembled. `ServerDependencies` is `createServer`'s to build -
8
+ * a host embedding chatter into its own server does so through
9
+ * `customRoutes` or by mounting the returned `ChatterApp`, not by calling
10
+ * `channel.start` directly with a hand-assembled `deps`.
9
11
  *
10
12
  * Published as the `@diegoaltoworks/chatter/channels` subpath alongside the
11
13
  * pure decision gates ({@link decideChannelAction} and friends) and the
@@ -42,7 +44,7 @@ export interface Channel {
42
44
  stop?(): void | Promise<void>;
43
45
  }
44
46
  export * from "./gates";
45
- export { createInboundPipeline, type InboundPipeline, type InboundPipelineConfig, type InboundPipelineDeps, type InboundPipelineOutcome, type InboundReplySender, type InboundTurn, resolveBrainHooks, } from "./pipeline";
47
+ export { conversationKeyFor, createInboundPipeline, type InboundPipeline, type InboundPipelineConfig, type InboundPipelineDeps, type InboundPipelineOutcome, type InboundReplySender, type InboundTurn, resolveBrainHooks, } from "./pipeline";
46
48
  export type { ChannelSender, ChannelSenderRegistry } from "./senders";
47
49
  export { createSenderRegistry } from "./senders";
48
50
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/channels/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,WAAW,OAAO;IACtB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/channels/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,WAAW,OAAO;IACtB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC"}
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/channels/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ conversationKeyFor: () => conversationKeyFor,
23
24
  createInboundPipeline: () => createInboundPipeline,
24
25
  createSenderRegistry: () => createSenderRegistry,
25
26
  createSlidingWindowRateLimiter: () => createSlidingWindowRateLimiter,
@@ -106,9 +107,10 @@ function detectLeakage(text) {
106
107
  function scrubOutput(text) {
107
108
  return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
108
109
  }
109
- function guardOutput(text) {
110
+ var DEFAULT_REFUSAL = "Sorry, I can't share internal instructions. How else can I help?";
111
+ function guardOutput(text, refusal) {
110
112
  if (detectLeakage(text)) {
111
- return "Sorry, I can't share internal instructions. How else can I help?";
113
+ return typeof refusal === "string" && refusal.trim() ? refusal : DEFAULT_REFUSAL;
112
114
  }
113
115
  return scrubOutput(text);
114
116
  }
@@ -120,7 +122,8 @@ async function completeOnce({
120
122
  system,
121
123
  messages,
122
124
  temperature = 0.2,
123
- model = DEFAULT_MODEL
125
+ model = DEFAULT_MODEL,
126
+ refusal
124
127
  }) {
125
128
  const res = await client.chat.completions.create({
126
129
  model,
@@ -129,19 +132,19 @@ async function completeOnce({
129
132
  });
130
133
  const text = res.choices[0]?.message?.content ?? "";
131
134
  return {
132
- content: guardOutput(text),
135
+ content: guardOutput(text, refusal),
133
136
  usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
134
137
  };
135
138
  }
136
139
 
137
140
  // src/core/answer.ts
138
141
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
139
- async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
142
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal) {
140
143
  const result = await answerFn({ system, messages, mode, sender, conversationId });
141
144
  const raw = typeof result === "string" ? result : result?.content;
142
145
  const usage = typeof result === "string" ? void 0 : result?.usage;
143
146
  return {
144
- content: guardOutput(typeof raw === "string" ? raw : ""),
147
+ content: guardOutput(typeof raw === "string" ? raw : "", refusal),
145
148
  usage: usage ?? noUsage()
146
149
  };
147
150
  }
@@ -154,12 +157,13 @@ async function answerOnce({
154
157
  sender,
155
158
  conversationId,
156
159
  temperature,
157
- model
160
+ model,
161
+ refusal
158
162
  }) {
159
163
  if (answerFn) {
160
- return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
164
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal);
161
165
  }
162
- return completeOnce({ client, system, messages, temperature, model });
166
+ return completeOnce({ client, system, messages, temperature, model, refusal });
163
167
  }
164
168
  async function applyTransformReply(transformReply, input, logger) {
165
169
  if (!transformReply) return input.text;
@@ -365,9 +369,17 @@ function resolveBrainHooks(config, fallback) {
365
369
  rewriteQuery: config.rewriteQuery ?? fallback?.rewriteQuery,
366
370
  rerankContext: config.rerankContext ?? fallback?.rerankContext,
367
371
  fallbackFn: config.fallbackFn ?? fallback?.fallbackFn,
368
- transformReply: config.transformReply ?? fallback?.transformReply
372
+ transformReply: config.transformReply ?? fallback?.transformReply,
373
+ refusal: config.refusal ?? fallback?.refusal
369
374
  };
370
375
  }
376
+ function escapeKeyPart(part) {
377
+ return part.replaceAll("%", "%25").replaceAll("#", "%23");
378
+ }
379
+ function conversationKeyFor(chatId, endpointId) {
380
+ if (!endpointId) return chatId;
381
+ return `${escapeKeyPart(chatId)}#${escapeKeyPart(endpointId)}`;
382
+ }
371
383
  var DEFAULT_HISTORY_LIMIT = 20;
372
384
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
373
385
  var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
@@ -426,11 +438,15 @@ function createInboundPipeline(deps, config) {
426
438
  return { action: "rate-limited" };
427
439
  }
428
440
  const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
429
- const conversationId = turn.conversationId ?? msg.chatId;
441
+ const conversationId = turn.conversationId ?? conversationKeyFor(msg.chatId, msg.endpointId);
430
442
  if (turn.intercept && await turn.intercept(sender)) {
431
443
  return { action: "intercepted" };
432
444
  }
433
- const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
445
+ const personaLayer = await resolvePersonaLayer(config, {
446
+ sender,
447
+ text: msg.text,
448
+ endpointId: msg.endpointId
449
+ });
434
450
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
435
451
  const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
436
452
  const priorTurns = config.history && historyEnabled ? await config.history.store.load(
@@ -463,7 +479,8 @@ function createInboundPipeline(deps, config) {
463
479
  mode,
464
480
  sender,
465
481
  conversationId,
466
- model: config.model
482
+ model: config.model,
483
+ refusal: config.refusal
467
484
  });
468
485
  const content = await applyTransformReply(
469
486
  config.transformReply,
@@ -569,6 +586,7 @@ function createSenderRegistry(logger = createConsoleLogger()) {
569
586
  }
570
587
  // Annotate the CommonJS export names for ESM import in node:
571
588
  0 && (module.exports = {
589
+ conversationKeyFor,
572
590
  createInboundPipeline,
573
591
  createSenderRegistry,
574
592
  createSlidingWindowRateLimiter,
@@ -72,9 +72,10 @@ function detectLeakage(text) {
72
72
  function scrubOutput(text) {
73
73
  return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
74
74
  }
75
- function guardOutput(text) {
75
+ var DEFAULT_REFUSAL = "Sorry, I can't share internal instructions. How else can I help?";
76
+ function guardOutput(text, refusal) {
76
77
  if (detectLeakage(text)) {
77
- return "Sorry, I can't share internal instructions. How else can I help?";
78
+ return typeof refusal === "string" && refusal.trim() ? refusal : DEFAULT_REFUSAL;
78
79
  }
79
80
  return scrubOutput(text);
80
81
  }
@@ -86,7 +87,8 @@ async function completeOnce({
86
87
  system,
87
88
  messages,
88
89
  temperature = 0.2,
89
- model = DEFAULT_MODEL
90
+ model = DEFAULT_MODEL,
91
+ refusal
90
92
  }) {
91
93
  const res = await client.chat.completions.create({
92
94
  model,
@@ -95,19 +97,19 @@ async function completeOnce({
95
97
  });
96
98
  const text = res.choices[0]?.message?.content ?? "";
97
99
  return {
98
- content: guardOutput(text),
100
+ content: guardOutput(text, refusal),
99
101
  usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
100
102
  };
101
103
  }
102
104
 
103
105
  // src/core/answer.ts
104
106
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
105
- async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
107
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal) {
106
108
  const result = await answerFn({ system, messages, mode, sender, conversationId });
107
109
  const raw = typeof result === "string" ? result : result?.content;
108
110
  const usage = typeof result === "string" ? void 0 : result?.usage;
109
111
  return {
110
- content: guardOutput(typeof raw === "string" ? raw : ""),
112
+ content: guardOutput(typeof raw === "string" ? raw : "", refusal),
111
113
  usage: usage ?? noUsage()
112
114
  };
113
115
  }
@@ -120,12 +122,13 @@ async function answerOnce({
120
122
  sender,
121
123
  conversationId,
122
124
  temperature,
123
- model
125
+ model,
126
+ refusal
124
127
  }) {
125
128
  if (answerFn) {
126
- return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
129
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal);
127
130
  }
128
- return completeOnce({ client, system, messages, temperature, model });
131
+ return completeOnce({ client, system, messages, temperature, model, refusal });
129
132
  }
130
133
  async function applyTransformReply(transformReply, input, logger) {
131
134
  if (!transformReply) return input.text;
@@ -331,9 +334,17 @@ function resolveBrainHooks(config, fallback) {
331
334
  rewriteQuery: config.rewriteQuery ?? fallback?.rewriteQuery,
332
335
  rerankContext: config.rerankContext ?? fallback?.rerankContext,
333
336
  fallbackFn: config.fallbackFn ?? fallback?.fallbackFn,
334
- transformReply: config.transformReply ?? fallback?.transformReply
337
+ transformReply: config.transformReply ?? fallback?.transformReply,
338
+ refusal: config.refusal ?? fallback?.refusal
335
339
  };
336
340
  }
341
+ function escapeKeyPart(part) {
342
+ return part.replaceAll("%", "%25").replaceAll("#", "%23");
343
+ }
344
+ function conversationKeyFor(chatId, endpointId) {
345
+ if (!endpointId) return chatId;
346
+ return `${escapeKeyPart(chatId)}#${escapeKeyPart(endpointId)}`;
347
+ }
337
348
  var DEFAULT_HISTORY_LIMIT = 20;
338
349
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
339
350
  var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
@@ -392,11 +403,15 @@ function createInboundPipeline(deps, config) {
392
403
  return { action: "rate-limited" };
393
404
  }
394
405
  const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
395
- const conversationId = turn.conversationId ?? msg.chatId;
406
+ const conversationId = turn.conversationId ?? conversationKeyFor(msg.chatId, msg.endpointId);
396
407
  if (turn.intercept && await turn.intercept(sender)) {
397
408
  return { action: "intercepted" };
398
409
  }
399
- const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
410
+ const personaLayer = await resolvePersonaLayer(config, {
411
+ sender,
412
+ text: msg.text,
413
+ endpointId: msg.endpointId
414
+ });
400
415
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
401
416
  const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
402
417
  const priorTurns = config.history && historyEnabled ? await config.history.store.load(
@@ -429,7 +444,8 @@ function createInboundPipeline(deps, config) {
429
444
  mode,
430
445
  sender,
431
446
  conversationId,
432
- model: config.model
447
+ model: config.model,
448
+ refusal: config.refusal
433
449
  });
434
450
  const content = await applyTransformReply(
435
451
  config.transformReply,
@@ -534,6 +550,7 @@ function createSenderRegistry(logger = createConsoleLogger()) {
534
550
  };
535
551
  }
536
552
  export {
553
+ conversationKeyFor,
537
554
  createInboundPipeline,
538
555
  createSenderRegistry,
539
556
  createSlidingWindowRateLimiter,
@@ -28,6 +28,8 @@ export interface MatrixChannelConfig extends PollingChannelConfig {
28
28
  personaResolver?: (ctx: {
29
29
  sender: string;
30
30
  text: string;
31
+ /** Which of this process's endpoints received the message - see `ChannelMessage.endpointId`. Unset unless the channel runs more than one. */
32
+ endpointId?: string;
31
33
  }) => string | undefined | Promise<string | undefined>;
32
34
  /** Auto-accept room invites, so the bot can actually receive messages in a room a user just invited it to. @default true */
33
35
  autoJoin?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQxD,MAAM,WAAW,mBAAoB,SAAQ,oBAAoB;IAC/D,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,4GAA4G;IAC5G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gIAAgI;IAChI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iHAAiH;IACjH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kHAAkH;IAClH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wGAAwG;IACxG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0MAA0M;IAC1M,GAAG,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CA8HxE"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQxD,MAAM,WAAW,mBAAoB,SAAQ,oBAAoB;IAC/D,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,yGAAyG;IACzG,WAAW,EAAE,MAAM,CAAC;IACpB,4GAA4G;IAC5G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gIAAgI;IAChI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iHAAiH;IACjH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,6IAA6I;QAC7I,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kHAAkH;IAClH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wGAAwG;IACxG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0MAA0M;IAC1M,GAAG,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAyIxE"}
@@ -6,6 +6,7 @@
6
6
  * rooms and events at once instead of one Telegram update.
7
7
  */
8
8
  import type { Logger } from "../../core/logger";
9
+ import { type SessionIdentityRegistry } from "../gates";
9
10
  import type { InboundPipeline } from "../pipeline";
10
11
  import type { ChannelSender } from "../senders";
11
12
  import type { MatrixApi, MatrixSyncResponse } from "./api";
@@ -14,6 +15,12 @@ export interface MatrixSyncHandlerDeps {
14
15
  api: MatrixApi;
15
16
  me: MatrixIdentity;
16
17
  pipeline: InboundPipeline;
18
+ /**
19
+ * Every identity this process answers to, across all its channels - what
20
+ * `fromBot` is resolved against, so another endpoint of the same process is
21
+ * recognised as "us". Omitted: `me` alone, which cannot see a second bot.
22
+ */
23
+ identities?: SessionIdentityRegistry;
17
24
  /** Group rooms eligible for a reply. Empty = every room. */
18
25
  allowedChats: string[];
19
26
  /**
@@ -44,6 +51,8 @@ export interface MatrixSyncHandlerDeps {
44
51
  logger: Logger;
45
52
  /** Log-line prefix, e.g. `Matrix[@bot:example.org]`. */
46
53
  label: string;
54
+ /** This channel's configured name - populates `ChannelMessage.endpointId`. Omitted: `endpointId` is left unset. */
55
+ channelName?: string;
47
56
  }
48
57
  /**
49
58
  * One `/sync` response -> zero or more pipeline turns, one per
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAKpB,MAAM,WAAW,CAAC;AAmBnB,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,SAAS,CAAC;IACf,EAAE,EAAE,cAAc,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;OAKG;IACH,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,GAC1B,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAsGjD;AAED,kHAAkH;AAClH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAkB3F"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAwB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAKpB,MAAM,WAAW,CAAC;AAmBnB,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,SAAS,CAAC;IACf,EAAE,EAAE,cAAc,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAC;IACrC,4DAA4D;IAC5D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;OAKG;IACH,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,mHAAmH;IACnH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,GAC1B,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CA8GjD;AAED,kHAAkH;AAClH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAkB3F"}
@@ -33,5 +33,5 @@ export { createMatrixApi, DEFAULT_MAX_MEDIA_BYTES, type MatrixAccountDataEvent,
33
33
  export { createMatrixChannel, type MatrixChannelConfig } from "./channel";
34
34
  export { createMatrixSender, createMatrixSyncHandler, type MatrixSyncHandlerDeps, } from "./handler";
35
35
  export { retryDelayMs, runMatrixSyncLoop, syncBackoffMs } from "./sync";
36
- export { directInviteFrom, directMappingFromEvents, directMappingRooms, directRoomIds, isReplyToBot, MAX_TRACKED_SENT_EVENTS, type MatrixDirectMapping, type MatrixIdentity, matrixSenderKey, mentionsBot, messageText, recordSentEventId, toChannelMessage, toDirectMapping, withDirectRoom, } from "./updates";
36
+ export { directInviteFrom, directMappingFromEvents, directMappingRooms, directRoomIds, isReplyToBot, MAX_TRACKED_SENT_EVENTS, type MatrixDirectMapping, type MatrixIdentity, matrixOwnIdentities, matrixSenderKey, mentionsBot, messageText, recordSentEventId, toChannelMessage, toDirectMapping, withDirectRoom, } from "./updates";
37
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,WAAW,GACZ,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,WAAW,GACZ,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,WAAW,CAAC"}
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  directMappingRooms: () => directMappingRooms,
33
33
  directRoomIds: () => directRoomIds,
34
34
  isReplyToBot: () => isReplyToBot,
35
+ matrixOwnIdentities: () => matrixOwnIdentities,
35
36
  matrixSenderKey: () => matrixSenderKey,
36
37
  mentionsBot: () => mentionsBot,
37
38
  messageText: () => messageText,
@@ -343,9 +344,10 @@ function detectLeakage(text) {
343
344
  function scrubOutput(text) {
344
345
  return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
345
346
  }
346
- function guardOutput(text) {
347
+ var DEFAULT_REFUSAL = "Sorry, I can't share internal instructions. How else can I help?";
348
+ function guardOutput(text, refusal) {
347
349
  if (detectLeakage(text)) {
348
- return "Sorry, I can't share internal instructions. How else can I help?";
350
+ return typeof refusal === "string" && refusal.trim() ? refusal : DEFAULT_REFUSAL;
349
351
  }
350
352
  return scrubOutput(text);
351
353
  }
@@ -357,7 +359,8 @@ async function completeOnce({
357
359
  system,
358
360
  messages,
359
361
  temperature = 0.2,
360
- model = DEFAULT_MODEL
362
+ model = DEFAULT_MODEL,
363
+ refusal
361
364
  }) {
362
365
  const res = await client.chat.completions.create({
363
366
  model,
@@ -366,19 +369,19 @@ async function completeOnce({
366
369
  });
367
370
  const text = res.choices[0]?.message?.content ?? "";
368
371
  return {
369
- content: guardOutput(text),
372
+ content: guardOutput(text, refusal),
370
373
  usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
371
374
  };
372
375
  }
373
376
 
374
377
  // src/core/answer.ts
375
378
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
376
- async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
379
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal) {
377
380
  const result = await answerFn({ system, messages, mode, sender, conversationId });
378
381
  const raw = typeof result === "string" ? result : result?.content;
379
382
  const usage = typeof result === "string" ? void 0 : result?.usage;
380
383
  return {
381
- content: guardOutput(typeof raw === "string" ? raw : ""),
384
+ content: guardOutput(typeof raw === "string" ? raw : "", refusal),
382
385
  usage: usage ?? noUsage()
383
386
  };
384
387
  }
@@ -391,12 +394,13 @@ async function answerOnce({
391
394
  sender,
392
395
  conversationId,
393
396
  temperature,
394
- model
397
+ model,
398
+ refusal
395
399
  }) {
396
400
  if (answerFn) {
397
- return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
401
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal);
398
402
  }
399
- return completeOnce({ client, system, messages, temperature, model });
403
+ return completeOnce({ client, system, messages, temperature, model, refusal });
400
404
  }
401
405
  async function applyTransformReply(transformReply, input, logger) {
402
406
  if (!transformReply) return input.text;
@@ -643,6 +647,16 @@ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now)
643
647
  function underReplyRateLimit(msg, limiters) {
644
648
  return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
645
649
  }
650
+ function isFromAnySession(id, registry) {
651
+ if (!id) return false;
652
+ for (const ids of registry.values()) {
653
+ if (ids.includes(id)) return true;
654
+ }
655
+ return false;
656
+ }
657
+ function isEffectivelyFromSelf(msg, registry) {
658
+ return msg.fromBot || isFromAnySession(msg.senderId, registry);
659
+ }
646
660
 
647
661
  // src/channels/pipeline.ts
648
662
  function resolveBrainHooks(config, fallback) {
@@ -652,9 +666,17 @@ function resolveBrainHooks(config, fallback) {
652
666
  rewriteQuery: config.rewriteQuery ?? fallback?.rewriteQuery,
653
667
  rerankContext: config.rerankContext ?? fallback?.rerankContext,
654
668
  fallbackFn: config.fallbackFn ?? fallback?.fallbackFn,
655
- transformReply: config.transformReply ?? fallback?.transformReply
669
+ transformReply: config.transformReply ?? fallback?.transformReply,
670
+ refusal: config.refusal ?? fallback?.refusal
656
671
  };
657
672
  }
673
+ function escapeKeyPart(part) {
674
+ return part.replaceAll("%", "%25").replaceAll("#", "%23");
675
+ }
676
+ function conversationKeyFor(chatId, endpointId) {
677
+ if (!endpointId) return chatId;
678
+ return `${escapeKeyPart(chatId)}#${escapeKeyPart(endpointId)}`;
679
+ }
658
680
  var DEFAULT_HISTORY_LIMIT = 20;
659
681
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
660
682
  var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
@@ -713,11 +735,15 @@ function createInboundPipeline(deps, config) {
713
735
  return { action: "rate-limited" };
714
736
  }
715
737
  const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
716
- const conversationId = turn.conversationId ?? msg.chatId;
738
+ const conversationId = turn.conversationId ?? conversationKeyFor(msg.chatId, msg.endpointId);
717
739
  if (turn.intercept && await turn.intercept(sender)) {
718
740
  return { action: "intercepted" };
719
741
  }
720
- const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
742
+ const personaLayer = await resolvePersonaLayer(config, {
743
+ sender,
744
+ text: msg.text,
745
+ endpointId: msg.endpointId
746
+ });
721
747
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
722
748
  const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
723
749
  const priorTurns = config.history && historyEnabled ? await config.history.store.load(
@@ -750,7 +776,8 @@ function createInboundPipeline(deps, config) {
750
776
  mode,
751
777
  sender,
752
778
  conversationId,
753
- model: config.model
779
+ model: config.model,
780
+ refusal: config.refusal
754
781
  });
755
782
  const content = await applyTransformReply(
756
783
  config.transformReply,
@@ -785,6 +812,7 @@ function defaultSleep(ms) {
785
812
  }
786
813
 
787
814
  // src/channels/matrix/updates.ts
815
+ var NO_IDENTITIES = /* @__PURE__ */ new Map();
788
816
  function messageText(content) {
789
817
  return content.body ?? "";
790
818
  }
@@ -852,7 +880,7 @@ function directInviteFrom(room, me) {
852
880
  }
853
881
  return void 0;
854
882
  }
855
- function toChannelMessage(roomId, event, me, directRooms, sentEventIds) {
883
+ function toChannelMessage(roomId, event, me, directRooms, sentEventIds, identities = NO_IDENTITIES, channelName) {
856
884
  if (event.type !== "m.room.message") return void 0;
857
885
  const content = event.content;
858
886
  if (!content.msgtype) return void 0;
@@ -864,10 +892,22 @@ function toChannelMessage(roomId, event, me, directRooms, sentEventIds) {
864
892
  isDirectMessage: directRooms.has(roomId),
865
893
  mentionsBot: mentionsBot(content, me),
866
894
  isReplyToBot: isReplyToBot(content, sentEventIds),
867
- fromBot: event.sender === me.userId,
868
- messageRef: event.event_id
895
+ // One access token is one user, but one process is not: a second bot
896
+ // account mounted alongside this one is a stranger to `me` and would be
897
+ // answered, and would answer back, forever. `identities` is what makes
898
+ // the other endpoint's user id "us" too - an empty one degenerates to
899
+ // the equality check.
900
+ fromBot: isEffectivelyFromSelf(
901
+ { fromBot: event.sender === me.userId, senderId: event.sender },
902
+ identities
903
+ ),
904
+ messageRef: event.event_id,
905
+ endpointId: channelName
869
906
  };
870
907
  }
908
+ function matrixOwnIdentities(me) {
909
+ return [me.userId];
910
+ }
871
911
  function matrixSenderKey(userId) {
872
912
  return `mx:${userId}`;
873
913
  }
@@ -882,7 +922,7 @@ async function sendText(api, roomId, text, sentEventIds, replyToEventId) {
882
922
  recordSentEventId(sentEventIds, eventId);
883
923
  }
884
924
  function createMatrixSyncHandler(deps) {
885
- const { api, me, pipeline, allowedChats, sentEventIds, logger, label } = deps;
925
+ const { api, me, pipeline, identities, allowedChats, sentEventIds, logger, label, channelName } = deps;
886
926
  const autoJoin = deps.autoJoin ?? true;
887
927
  const loggedUnallowedChats = /* @__PURE__ */ new Set();
888
928
  const loggedDeclinedInvites = /* @__PURE__ */ new Set();
@@ -927,7 +967,15 @@ function createMatrixSyncHandler(deps) {
927
967
  }
928
968
  for (const [roomId, room] of Object.entries(response.rooms?.join ?? {})) {
929
969
  for (const event of room.timeline.events) {
930
- const msg = toChannelMessage(roomId, event, me, directRooms, sentEventIds);
970
+ const msg = toChannelMessage(
971
+ roomId,
972
+ event,
973
+ me,
974
+ directRooms,
975
+ sentEventIds,
976
+ identities,
977
+ channelName
978
+ );
931
979
  if (!msg) continue;
932
980
  if (isBlockedByAllowlist(msg, { allowedChats })) {
933
981
  if (!loggedUnallowedChats.has(msg.chatId)) {
@@ -944,7 +992,6 @@ function createMatrixSyncHandler(deps) {
944
992
  try {
945
993
  await pipeline(msg, {
946
994
  reply,
947
- conversationId: msg.chatId,
948
995
  sender: matrixSenderKey(msg.senderId)
949
996
  });
950
997
  } catch (error) {
@@ -1040,6 +1087,8 @@ function createMatrixChannel(config) {
1040
1087
  });
1041
1088
  const me = await api.whoami();
1042
1089
  const label = `Matrix[${me.userId}]`;
1090
+ const identities = config.identities ?? deps.identities;
1091
+ identities.set(channelName, matrixOwnIdentities(me));
1043
1092
  let initialDirect;
1044
1093
  try {
1045
1094
  const direct = await api.getAccountData(me.userId, "m.direct");
@@ -1073,13 +1122,18 @@ function createMatrixChannel(config) {
1073
1122
  const handleSync = createMatrixSyncHandler({
1074
1123
  api,
1075
1124
  me,
1125
+ identities,
1076
1126
  pipeline,
1077
1127
  allowedChats,
1078
1128
  sentEventIds,
1079
1129
  autoJoin: config.autoJoin,
1080
1130
  initialDirect,
1081
1131
  logger: log,
1082
- label
1132
+ label,
1133
+ // config.name, not the resolved channelName: endpointId opts in only
1134
+ // when the host explicitly named this channel (running more than
1135
+ // one bot in the process), not for a default single-token channel.
1136
+ channelName: config.name
1083
1137
  });
1084
1138
  abort = new AbortController();
1085
1139
  const signal = abort.signal;
@@ -1119,6 +1173,7 @@ function createMatrixChannel(config) {
1119
1173
  directMappingRooms,
1120
1174
  directRoomIds,
1121
1175
  isReplyToBot,
1176
+ matrixOwnIdentities,
1122
1177
  matrixSenderKey,
1123
1178
  mentionsBot,
1124
1179
  messageText,