@openclaw/matrix 2026.7.2-beta.3 → 2026.7.2-beta.5

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 (77) hide show
  1. package/README.md +1 -1
  2. package/dist/api.js +4 -4
  3. package/dist/{approval-handler.runtime-BEa98wwG.js → approval-handler.runtime-D1ibJgSk.js} +4 -4
  4. package/dist/{approval-ids-Dfn7oQAm.js → approval-ids-2OQj32-K.js} +1 -1
  5. package/dist/{approval-reaction-auth-kXkCJBGi.js → approval-reaction-auth-CCAWWkbu.js} +2 -2
  6. package/dist/{channel-j331Xd7b.js → channel-Cmi248PA.js} +34 -32
  7. package/dist/channel-plugin-api.js +1 -1
  8. package/dist/{channel.runtime-BIwlVfQw.js → channel.runtime-BaPXFRgT.js} +34 -40
  9. package/dist/{cli-0EcKfqFw.js → cli-0CNHkps5.js} +868 -852
  10. package/dist/{cli-metadata-Xq1Xb15R.js → cli-metadata-DnXxNCoj.js} +1 -1
  11. package/dist/cli-metadata.js +1 -1
  12. package/dist/{client-CA0PULls.js → client-BFRg0kd4.js} +2 -2
  13. package/dist/{client-BtG4quUl.js → client-CitERujx.js} +2 -2
  14. package/dist/{client-bootstrap-Dk5EAn3n.js → client-bootstrap-CkAnkzB6.js} +1 -1
  15. package/dist/{config-schema-B5GRV8wL.js → config-schema-BShrVROw.js} +19 -6
  16. package/dist/contract-api.js +2 -2
  17. package/dist/{create-client-WQExaby_.js → create-client-f-CkRllE.js} +3 -3
  18. package/dist/{crypto-runtime-CAff-n15.js → crypto-runtime-D9_6J5tZ.js} +2 -2
  19. package/dist/{device-health-D4LBxuPq.js → device-health-B4-YXtJR.js} +6 -1
  20. package/dist/{directory-live-DiC-0XBC.js → directory-live-YRgYDx2w.js} +3 -3
  21. package/dist/{doctor-DwUVm5Lw.js → doctor-CM8ijNpW.js} +1 -1
  22. package/dist/doctor-contract-BMSJkkAE.js +167 -0
  23. package/dist/doctor-contract-api.js +112 -71
  24. package/dist/{draft-stream-CTLKtJJ0.js → draft-stream-CIjafRr0.js} +1 -1
  25. package/dist/{file-sync-store-CPkgnSXp.js → file-sync-store-DiiFsayX.js} +1 -1
  26. package/dist/{http-client-BmrgOZTL.js → http-client-CycYxU9-.js} +2 -2
  27. package/dist/{idb-persistence-Dyy6ipgU.js → idb-persistence-B6x7dMsF.js} +75 -72
  28. package/dist/index.js +1 -1
  29. package/dist/{logging-QH-48Gr6.js → logging-DV-p52N0.js} +8 -1
  30. package/dist/{media-text-BMXEXb1j.js → media-text-DlQC_k_K.js} +1 -1
  31. package/dist/{messages-3UBHfZrN.js → messages-Bf_s--M9.js} +3 -3
  32. package/dist/{monitor-BtUIds8T.js → monitor-s--XqoxF.js} +2041 -1804
  33. package/dist/plugin-entry.handlers.runtime.js +1 -1
  34. package/dist/probe.runtime-BJOvCHnZ.js +3 -0
  35. package/dist/{profile-update-Bh2XtJwN.js → profile-update-z3aM46KS.js} +2 -2
  36. package/dist/{reaction-events-BwAKtUyQ.js → reaction-events-DrK9Zua7.js} +2 -2
  37. package/dist/{recovery-key-store-CW6eePfp.js → recovery-key-store-BNnHvXdk.js} +1 -1
  38. package/dist/{resolve-targets-DVoj-YxB.js → resolve-targets-nNMcrV3W.js} +1 -1
  39. package/dist/{resolver.runtime-CC0VVsAt.js → resolver.runtime-BvaFlVLg.js} +1 -1
  40. package/dist/{room-info-B6vKD5DF.js → room-info-CAFZ3Z9J.js} +13 -12
  41. package/dist/{sdk-CIDZXWpZ.js → sdk-UzPd8tst.js} +227 -181
  42. package/dist/{send-BJbrdmNm.js → send-C9Ssx-iV.js} +624 -109
  43. package/dist/session-binding-contract-api.js +1 -1
  44. package/dist/{setup-bootstrap-DGPOOdz-.js → setup-bootstrap-BBGpPKgy.js} +2 -2
  45. package/dist/{setup-core-XCdAx1nY.js → setup-core-a542Ernq.js} +135 -45
  46. package/dist/setup-plugin-api.js +4 -3
  47. package/dist/{setup-surface-CzUiTe_k.js → setup-surface-BJrCI6KT.js} +34 -30
  48. package/dist/{shared-DquM1oz6.js → shared-CUH5otj8.js} +5 -12
  49. package/dist/{startup-verification-CA00kebL.js → startup-verification-wm3_i88s.js} +1 -1
  50. package/dist/{storage-CzHxeF5E.js → storage-BvAkgzvT.js} +24 -57
  51. package/dist/{thread-bindings-Cw194yEn.js → thread-bindings-D3RfVgAF.js} +4 -3
  52. package/dist/{tool-actions.runtime-C0jgWYhX.js → tool-actions.runtime-Dh5ow2EK.js} +8 -8
  53. package/dist/{verification-DjxyWTNP.js → verification-ZdAXdmuh.js} +1 -1
  54. package/node_modules/music-metadata/lib/common/Util.d.ts +1 -1
  55. package/node_modules/music-metadata/lib/common/Util.js +7 -1
  56. package/node_modules/music-metadata/lib/id3v2/FrameHeader.js +23 -3
  57. package/node_modules/music-metadata/lib/id3v2/FrameParser.js +2 -2
  58. package/node_modules/music-metadata/lib/id3v2/ID3v2Token.js +1 -1
  59. package/node_modules/music-metadata/lib/mp4/AtomToken.js +38 -13
  60. package/node_modules/music-metadata/lib/mpeg/MpegParser.js +1 -1
  61. package/node_modules/music-metadata/package.json +6 -6
  62. package/node_modules/typebox/build/compile/validator.d.mts +1 -4
  63. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  64. package/node_modules/typebox/build/guard/guard.mjs +5 -2
  65. package/node_modules/typebox/build/guard/string.mjs +16 -4
  66. package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
  67. package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
  68. package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
  69. package/node_modules/typebox/build/typebox.d.mts +1 -1
  70. package/node_modules/typebox/build/typebox.mjs +1 -1
  71. package/node_modules/typebox/package.json +1 -1
  72. package/node_modules/typebox/readme.md +29 -32
  73. package/openclaw.plugin.json +7 -20
  74. package/package.json +111 -29
  75. package/dist/doctor-contract-BA2Y6Crx.js +0 -312
  76. package/dist/probe.runtime-sBV-Govx.js +0 -3
  77. package/npm-shrinkwrap.json +0 -557
@@ -1,33 +1,34 @@
1
1
  import { a as resolveMatrixAccountConfig, i as resolveMatrixAccountAllowlistConfig } from "./account-config-BZWN9tGS.js";
2
2
  import { t as getMatrixRuntime } from "./runtime-6S3DNFNv.js";
3
- import { m as resolveConfiguredMatrixBotUserIds } from "./setup-core-XCdAx1nY.js";
4
- import { a as buildAllowlistResolutionSummary, c as createTypingCallbacks, d as logInboundDrop, f as logTypingFailure, h as toLocationContext, i as addAllowlistUserEntriesFromConfigEntry, l as formatLocationText, m as summarizeMapping, n as resolveMatrixStoredSessionMeta, o as canonicalizeAllowlistWithResolvedIds, p as patchAllowlistUsersInConfigEntries, r as resolveMatrixRoomConfig, s as createReplyPrefixOptions, u as getAgentScopedMediaLocalRoots } from "./channel-j331Xd7b.js";
5
- import { a as normalizeMatrixUserId, i as normalizeMatrixAllowList, o as resolveMatrixAllowListMatch } from "./config-schema-B5GRV8wL.js";
3
+ import { h as resolveConfiguredMatrixBotUserIds } from "./setup-core-a542Ernq.js";
4
+ import { a as buildAllowlistResolutionSummary, c as createTypingCallbacks, d as logInboundDrop, f as logTypingFailure, h as toLocationContext, i as addAllowlistUserEntriesFromConfigEntry, l as formatLocationText, m as summarizeMapping, n as resolveMatrixStoredSessionMeta, o as canonicalizeAllowlistWithResolvedIds, p as patchAllowlistUsersInConfigEntries, r as resolveMatrixRoomConfig, s as createReplyPrefixOptions, u as getAgentScopedMediaLocalRoots } from "./channel-Cmi248PA.js";
5
+ import { a as normalizeMatrixUserId, i as normalizeMatrixAllowList, o as resolveMatrixAllowListMatch } from "./config-schema-BShrVROw.js";
6
6
  import { t as isMatrixQualifiedUserId } from "./target-ids-B-5aQxwn.js";
7
7
  import { t as formatMatrixErrorMessage } from "./errors-C47hvAF8.js";
8
- import { D as parsePollStartContent, S as formatPollAsText, T as isPollStartType, _ as readJoinedMatrixMembers, a as sendMessageMatrix, f as promoteMatrixDirectRoomCandidate, g as isStrictDirectMembership, h as hasDirectMatrixMemberFlag, t as chunkMatrixText, v as MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY, w as isPollEventType } from "./send-BJbrdmNm.js";
9
- import { t as createMatrixThreadBindingManager } from "./thread-bindings-Cw194yEn.js";
8
+ import { D as parsePollStartContent, S as formatPollAsText, T as isPollStartType, g as MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY, h as readJoinedMatrixMembers, m as isStrictDirectMembership, p as hasDirectMatrixMemberFlag, r as sendMessageMatrix, u as promoteMatrixDirectRoomCandidate, v as chunkMatrixText, w as isPollEventType } from "./send-C9Ssx-iV.js";
9
+ import { t as createMatrixThreadBindingManager } from "./thread-bindings-D3RfVgAF.js";
10
10
  import { i as createMatrixInboundEventDeduper } from "./inbound-dedupe-CaBLblef.js";
11
- import { a as resolveMatrixMessageAttachment, c as fetchMatrixPollSnapshot, i as isLikelyBareFilename, n as formatMatrixMediaUnavailableText, o as resolveMatrixMessageBody, r as formatMatrixMessageText, t as formatMatrixMediaTooLargeText } from "./media-text-BMXEXb1j.js";
11
+ import { a as resolveMatrixMessageAttachment, c as fetchMatrixPollSnapshot, i as isLikelyBareFilename, n as formatMatrixMediaUnavailableText, o as resolveMatrixMessageBody, r as formatMatrixMessageText, t as formatMatrixMediaTooLargeText } from "./media-text-DlQC_k_K.js";
12
12
  import { n as setActiveMatrixClient } from "./active-client-DXvz2gCo.js";
13
13
  import { t as isBunRuntime } from "./runtime-BefyhPWv.js";
14
- import { d as backfillMatrixAuthDeviceIdAfterStartup, f as resolveMatrixAuth, i as resolveSharedMatrixClient, l as isMatrixStartupAbortError, n as releaseSharedClientInstance, p as resolveMatrixAuthContext, u as throwIfMatrixStartupAborted } from "./shared-DquM1oz6.js";
15
- import "./client-CA0PULls.js";
16
- import { i as isMatrixMediaSizeLimitError, r as MatrixMediaSizeLimitError } from "./http-client-BmrgOZTL.js";
14
+ import { d as backfillMatrixAuthDeviceIdAfterStartup, f as resolveMatrixAuth, i as resolveSharedMatrixClient, l as isMatrixStartupAbortError, n as releaseSharedClientInstance, p as resolveMatrixAuthContext, u as throwIfMatrixStartupAborted } from "./shared-CUH5otj8.js";
15
+ import "./client-BFRg0kd4.js";
16
+ import { i as isMatrixMediaSizeLimitError, r as MatrixMediaSizeLimitError } from "./http-client-CycYxU9-.js";
17
17
  import { d as resolveAllowlistProviderRuntimeGroupPolicy, f as resolveDefaultGroupPolicy, g as warnMissingProviderGroupPolicyFallbackOnce, h as resolveThreadBindingMaxAgeMsForChannel, m as resolveThreadBindingIdleTimeoutMsForChannel, t as GROUP_POLICY_BLOCKED_LABEL } from "./runtime-api-D1xI_cvh.js";
18
- import { t as resolveMatrixTargets } from "./resolve-targets-DVoj-YxB.js";
18
+ import { t as resolveMatrixTargets } from "./resolve-targets-nNMcrV3W.js";
19
19
  import { t as formatMatrixEncryptedEventDisabledWarning } from "./encryption-guidance-CbEFe38F.js";
20
20
  import { n as isMatrixReadySyncState, r as isMatrixTerminalSyncState, t as isMatrixDisconnectedSyncState } from "./sync-state-CWbp0QSY.js";
21
+ import { n as setBoundedMap, t as createMatrixRoomInfoResolver } from "./room-info-CAFZ3Z9J.js";
21
22
  import { a as EventType, i as resolveMatrixThreadRouting, n as resolveMatrixReplyToEventId, o as RelationType, r as resolveMatrixThreadRootId, t as resolveMatrixInboundRoute } from "./route-DY6at4qJ.js";
22
- import { t as createMatrixRoomInfoResolver } from "./room-info-B6vKD5DF.js";
23
23
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
24
24
  import { buildChannelProgressDraftLineForEntry, createChannelProgressDraftGate, createPreviewMessageReceipt, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftLine, formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, mergeChannelProgressDraftLine, normalizeChannelProgressDraftLineIdentity, resolveChannelProgressDraftMaxLines, resolveChannelStreamingBlockEnabled, waitUntilAbort } from "openclaw/plugin-sdk/channel-outbound";
25
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringifiedEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
25
+ import { normalizeLowercaseStringOrEmpty, normalizeNullableString, normalizeOptionalString, normalizeStringifiedEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
26
26
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
27
27
  import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
28
28
  import { buildChannelInboundEventContext, createChannelInboundEnvelopeBuilder, hasFinalInboundReplyDispatch, resolveInboundMentionDecision, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
29
29
  import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
30
30
  import { isFutureDateTimestampMs, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs, resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
31
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
31
32
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
32
33
  import { format } from "node:util";
33
34
  import { escapeRegExp, sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -36,13 +37,12 @@ import { resolveAckReaction } from "openclaw/plugin-sdk/channel-feedback";
36
37
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
37
38
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
38
39
  import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
39
- import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
40
40
  import { evaluateSupplementalContextVisibility, resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
41
+ import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
42
+ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
41
43
  import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
42
44
  import { buildInboundHistoryFromEntries } from "openclaw/plugin-sdk/reply-history";
43
- import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
44
45
  import { decodeHtmlEntities } from "openclaw/plugin-sdk/html-entity-runtime";
45
- import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
46
46
  import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
47
47
  //#region extensions/matrix/src/matrix/monitor/auto-join.ts
48
48
  function registerMatrixAutoJoin(params) {
@@ -466,15 +466,8 @@ async function resolveMatrixMonitorConfig(params) {
466
466
  //#region extensions/matrix/src/matrix/monitor/direct.ts
467
467
  const DM_CACHE_TTL_MS = 3e4;
468
468
  const RECENT_INVITE_TTL_MS = 3e4;
469
- const MAX_TRACKED_DM_ROOMS = 1024;
470
- const MAX_TRACKED_DM_MEMBER_FLAGS = 2048;
471
- function rememberBounded(map, key, value, maxSize = MAX_TRACKED_DM_ROOMS) {
472
- map.set(key, value);
473
- if (map.size > maxSize) {
474
- const oldest = map.keys().next().value;
475
- if (typeof oldest === "string") map.delete(oldest);
476
- }
477
- }
469
+ const MAX_DM_ROOMS = 1024;
470
+ const MAX_DM_MEMBER_FLAGS = 2048;
478
471
  function createDirectRoomTracker(client, opts = {}) {
479
472
  const log = opts.log ?? (() => {});
480
473
  let lastDmUpdateMs = 0;
@@ -506,10 +499,10 @@ function createDirectRoomTracker(client, opts = {}) {
506
499
  try {
507
500
  const normalized = await readJoinedMatrixMembers(client, roomId);
508
501
  if (!normalized) throw new Error("membership unavailable");
509
- rememberBounded(joinedMembersCache, roomId, {
502
+ setBoundedMap(joinedMembersCache, roomId, {
510
503
  members: normalized,
511
504
  ts: now
512
- });
505
+ }, MAX_DM_ROOMS);
513
506
  return normalized;
514
507
  } catch (err) {
515
508
  log(`matrix: dm member lookup failed room=${roomId} (${String(err)})`);
@@ -524,10 +517,10 @@ function createDirectRoomTracker(client, opts = {}) {
524
517
  const now = Date.now();
525
518
  if (cached && now - cached.ts < DM_CACHE_TTL_MS) return cached.isDirect;
526
519
  const isDirect = await hasDirectMatrixMemberFlag(client, roomId, normalizedUserId);
527
- rememberBounded(directMemberFlagCache, cacheKey, {
520
+ setBoundedMap(directMemberFlagCache, cacheKey, {
528
521
  isDirect,
529
522
  ts: now
530
- }, MAX_TRACKED_DM_MEMBER_FLAGS);
523
+ }, MAX_DM_MEMBER_FLAGS);
531
524
  return isDirect;
532
525
  };
533
526
  const hasRecentInviteCandidate = (roomId, remoteUserId) => {
@@ -581,7 +574,7 @@ function createDirectRoomTracker(client, opts = {}) {
581
574
  const rememberLocallyPromotedDirectRoom = (roomId, remoteUserId) => {
582
575
  const normalizedRemoteUserId = remoteUserId.trim();
583
576
  if (!normalizedRemoteUserId) return;
584
- rememberBounded(locallyPromotedDirectRooms, roomId, { remoteUserId: normalizedRemoteUserId });
577
+ setBoundedMap(locallyPromotedDirectRooms, roomId, { remoteUserId: normalizedRemoteUserId }, MAX_DM_ROOMS);
585
578
  };
586
579
  return {
587
580
  invalidateRoom: (roomId) => {
@@ -593,10 +586,10 @@ function createDirectRoomTracker(client, opts = {}) {
593
586
  rememberInvite: (roomId, remoteUserId) => {
594
587
  const normalizedRemoteUserId = remoteUserId.trim();
595
588
  if (!normalizedRemoteUserId) return;
596
- rememberBounded(recentInviteCandidates, roomId, {
589
+ setBoundedMap(recentInviteCandidates, roomId, {
597
590
  remoteUserId: normalizedRemoteUserId,
598
591
  ts: Date.now()
599
- });
592
+ }, MAX_DM_ROOMS);
600
593
  log(`matrix: remembered invite candidate room=${roomId} sender=${normalizedRemoteUserId}`);
601
594
  },
602
595
  isDirectMessage: async (params) => {
@@ -793,21 +786,21 @@ const VERIFICATION_NOTICE_PREFIXES = [
793
786
  "Matrix verification cancelled by ",
794
787
  "Matrix verification SAS with "
795
788
  ];
796
- function trimMaybeString$1(input) {
789
+ function trimMaybeString(input) {
797
790
  return normalizeOptionalString(input) ?? "";
798
791
  }
799
792
  function isMatrixVerificationEventType(type) {
800
- return trimMaybeString$1(type).startsWith(VERIFICATION_EVENT_PREFIX);
793
+ return trimMaybeString(type).startsWith(VERIFICATION_EVENT_PREFIX);
801
794
  }
802
795
  function isMatrixVerificationRequestMsgType(msgtype) {
803
- return trimMaybeString$1(msgtype) === VERIFICATION_REQUEST_MSGTYPE;
796
+ return trimMaybeString(msgtype) === VERIFICATION_REQUEST_MSGTYPE;
804
797
  }
805
798
  function isMatrixVerificationNoticeBody(body) {
806
- const text = trimMaybeString$1(body);
799
+ const text = trimMaybeString(body);
807
800
  return VERIFICATION_NOTICE_PREFIXES.some((prefix) => text.startsWith(prefix));
808
801
  }
809
802
  function isMatrixVerificationRoomMessage(content) {
810
- return isMatrixVerificationRequestMsgType(content.msgtype) || trimMaybeString$1(content.msgtype) === "m.notice" && isMatrixVerificationNoticeBody(content.body);
803
+ return isMatrixVerificationRequestMsgType(content.msgtype) || trimMaybeString(content.msgtype) === "m.notice" && isMatrixVerificationNoticeBody(content.body);
811
804
  }
812
805
  const matrixVerificationConstants = {
813
806
  eventPrefix: VERIFICATION_EVENT_PREFIX,
@@ -818,27 +811,22 @@ const matrixVerificationConstants = {
818
811
  const MAX_TRACKED_VERIFICATION_EVENTS = 1024;
819
812
  const SAS_NOTICE_RETRY_DELAY_MS = 750;
820
813
  const VERIFICATION_EVENT_STARTUP_GRACE_MS = 3e4;
821
- const loadMatrixDirectRoomDeps = createLazyRuntimeModule(() => Promise.all([import("./send-BJbrdmNm.js").then((n) => n.d), import("./send-BJbrdmNm.js").then((n) => n.m)]).then(([directManagementModule, directRoomModule]) => ({
814
+ const loadMatrixDirectRoomDeps = createLazyRuntimeModule(() => Promise.all([import("./send-C9Ssx-iV.js").then((n) => n.l), import("./send-C9Ssx-iV.js").then((n) => n.f)]).then(([directManagementModule, directRoomModule]) => ({
822
815
  inspectMatrixDirectRooms: directManagementModule.inspectMatrixDirectRooms,
823
816
  isStrictDirectRoom: directRoomModule.isStrictDirectRoom
824
817
  })));
825
- function trimMaybeString(input) {
826
- if (typeof input !== "string") return null;
827
- const trimmed = input.trim();
828
- return trimmed.length > 0 ? trimmed : null;
829
- }
830
818
  function readVerificationSignal(event) {
831
- const type = trimMaybeString(event?.type) ?? "";
819
+ const type = normalizeNullableString(event?.type) ?? "";
832
820
  const content = event?.content ?? {};
833
- const msgtype = trimMaybeString(content.msgtype) ?? "";
834
- const relatedEventId = trimMaybeString(content["m.relates_to"]?.event_id);
835
- const transactionId = trimMaybeString(content.transaction_id);
821
+ const msgtype = normalizeNullableString(content.msgtype) ?? "";
822
+ const relatedEventId = normalizeNullableString(content["m.relates_to"]?.event_id);
823
+ const transactionId = normalizeNullableString(content.transaction_id);
836
824
  if (type === EventType.RoomMessage && isMatrixVerificationRequestMsgType(msgtype)) return {
837
825
  stage: "request",
838
- flowId: trimMaybeString(event.event_id) ?? transactionId ?? relatedEventId
826
+ flowId: normalizeNullableString(event.event_id) ?? transactionId ?? relatedEventId
839
827
  };
840
828
  if (!isMatrixVerificationEventType(type)) return null;
841
- const flowId = transactionId ?? relatedEventId ?? trimMaybeString(event.event_id);
829
+ const flowId = transactionId ?? relatedEventId ?? normalizeNullableString(event.event_id);
842
830
  if (type === `${matrixVerificationConstants.eventPrefix}request`) return {
843
831
  stage: "request",
844
832
  flowId
@@ -873,8 +861,8 @@ function formatVerificationStageNotice(params) {
873
861
  case "start": return `Matrix verification started with ${senderId}.`;
874
862
  case "done": return `Matrix verification completed with ${senderId}.`;
875
863
  case "cancel": {
876
- const code = trimMaybeString(content.code);
877
- const reason = trimMaybeString(content.reason);
864
+ const code = normalizeNullableString(content.code);
865
+ const reason = normalizeNullableString(content.reason);
878
866
  if (code && reason) return `Matrix verification cancelled by ${senderId} (${code}: ${reason}).`;
879
867
  if (reason) return `Matrix verification cancelled by ${senderId} (${reason}).`;
880
868
  return `Matrix verification cancelled by ${senderId}.`;
@@ -885,7 +873,7 @@ function formatVerificationStageNotice(params) {
885
873
  function formatVerificationSasNotice(summary) {
886
874
  const sas = summary.sas;
887
875
  if (!sas) return null;
888
- const emojiLine = Array.isArray(sas.emoji) && sas.emoji.length > 0 ? `SAS emoji: ${sas.emoji.map(([emoji, name]) => `${trimMaybeString(emoji) ?? "?"} ${trimMaybeString(name) ?? "?"}`).join(" | ")}` : null;
876
+ const emojiLine = Array.isArray(sas.emoji) && sas.emoji.length > 0 ? `SAS emoji: ${sas.emoji.map(([emoji, name]) => `${normalizeNullableString(emoji) ?? "?"} ${normalizeNullableString(name) ?? "?"}`).join(" | ")}` : null;
889
877
  const decimalLine = Array.isArray(sas.decimal) && sas.decimal.length === 3 ? `SAS decimal: ${sas.decimal.join(" ")}` : null;
890
878
  if (!emojiLine && !decimalLine) return null;
891
879
  const lines = [`Matrix verification SAS with ${summary.otherUserId}:`];
@@ -899,7 +887,7 @@ function resolveVerificationFlowCandidates(params) {
899
887
  const content = event.content;
900
888
  const candidates = /* @__PURE__ */ new Set();
901
889
  const add = (value) => {
902
- const normalized = trimMaybeString(value);
890
+ const normalized = normalizeNullableString(value);
903
891
  if (normalized) candidates.add(normalized);
904
892
  };
905
893
  add(flowId);
@@ -933,7 +921,7 @@ async function resolveVerificationSummaryForSignal(client, params) {
933
921
  const byTransactionId = list.find((entry) => candidates.some((candidate) => entry.transactionId === candidate));
934
922
  if (byTransactionId) return byTransactionId;
935
923
  const { inspectMatrixDirectRooms, isStrictDirectRoom } = await loadMatrixDirectRoomDeps();
936
- const activeRoomId = trimMaybeString((await inspectMatrixDirectRooms({
924
+ const activeRoomId = normalizeNullableString((await inspectMatrixDirectRooms({
937
925
  client,
938
926
  remoteUserId: params.senderId
939
927
  }).catch(() => null))?.activeRoomId);
@@ -946,7 +934,7 @@ async function resolveVerificationSummaryForSignal(client, params) {
946
934
  })) return null;
947
935
  const activeByUser = list.filter((entry) => entry.otherUserId === params.senderId && isActiveVerificationSummary(entry)).toSorted((a, b) => resolveSummaryRecency(b) - resolveSummaryRecency(a));
948
936
  const activeInRoom = activeByUser.filter((entry) => {
949
- return trimMaybeString(entry.roomId) === params.roomId;
937
+ return normalizeNullableString(entry.roomId) === params.roomId;
950
938
  });
951
939
  if (activeInRoom.length > 0) return activeInRoom[0] ?? null;
952
940
  return activeByUser[0] ?? null;
@@ -977,7 +965,7 @@ function trackBounded(set, value) {
977
965
  return true;
978
966
  }
979
967
  async function sendVerificationNotice(params) {
980
- const roomId = trimMaybeString(params.roomId);
968
+ const roomId = normalizeNullableString(params.roomId);
981
969
  if (!roomId) return;
982
970
  try {
983
971
  await params.client.sendMessage(roomId, {
@@ -1016,7 +1004,7 @@ function createMatrixVerificationEventRouter(params) {
1016
1004
  const verificationUserRooms = /* @__PURE__ */ new Map();
1017
1005
  async function resolveActiveDirectRoomId(remoteUserId) {
1018
1006
  const { inspectMatrixDirectRooms } = await loadMatrixDirectRoomDeps();
1019
- return trimMaybeString((await inspectMatrixDirectRooms({
1007
+ return normalizeNullableString((await inspectMatrixDirectRooms({
1020
1008
  client: params.client,
1021
1009
  remoteUserId
1022
1010
  }).catch(() => null))?.activeRoomId);
@@ -1039,8 +1027,8 @@ function createMatrixVerificationEventRouter(params) {
1039
1027
  }
1040
1028
  }
1041
1029
  function rememberVerificationUserRoom(remoteUserId, roomId) {
1042
- const normalizedUserId = trimMaybeString(remoteUserId);
1043
- const normalizedRoomId = trimMaybeString(roomId);
1030
+ const normalizedUserId = normalizeNullableString(remoteUserId);
1031
+ const normalizedRoomId = normalizeNullableString(roomId);
1044
1032
  if (!normalizedUserId || !normalizedRoomId) return;
1045
1033
  verificationUserRooms.delete(normalizedUserId);
1046
1034
  verificationUserRooms.set(normalizedUserId, normalizedRoomId);
@@ -1050,11 +1038,11 @@ function createMatrixVerificationEventRouter(params) {
1050
1038
  }
1051
1039
  }
1052
1040
  async function resolveSummaryRoomId(summary) {
1053
- const mappedRoomId = trimMaybeString(summary.roomId) ?? trimMaybeString(summary.transactionId ? verificationFlowRooms.get(summary.transactionId) : null) ?? trimMaybeString(verificationFlowRooms.get(summary.id));
1041
+ const mappedRoomId = normalizeNullableString(summary.roomId) ?? normalizeNullableString(summary.transactionId ? verificationFlowRooms.get(summary.transactionId) : null) ?? normalizeNullableString(verificationFlowRooms.get(summary.id));
1054
1042
  if (mappedRoomId) return mappedRoomId;
1055
- const remoteUserId = trimMaybeString(summary.otherUserId);
1043
+ const remoteUserId = normalizeNullableString(summary.otherUserId);
1056
1044
  if (!remoteUserId) return null;
1057
- const recentRoomId = trimMaybeString(verificationUserRooms.get(remoteUserId));
1045
+ const recentRoomId = normalizeNullableString(verificationUserRooms.get(remoteUserId));
1058
1046
  const activeRoomId = await resolveActiveDirectRoomId(remoteUserId);
1059
1047
  if (recentRoomId && activeRoomId && recentRoomId === activeRoomId) return recentRoomId;
1060
1048
  if (activeRoomId) return activeRoomId;
@@ -1096,7 +1084,7 @@ function createMatrixVerificationEventRouter(params) {
1096
1084
  });
1097
1085
  }
1098
1086
  function routeVerificationEvent(roomId, event) {
1099
- const senderId = trimMaybeString(event?.sender);
1087
+ const senderId = normalizeNullableString(event?.sender);
1100
1088
  if (!senderId) return false;
1101
1089
  const signal = readVerificationSignal(event);
1102
1090
  if (!signal) return false;
@@ -1107,7 +1095,7 @@ function createMatrixVerificationEventRouter(params) {
1107
1095
  return;
1108
1096
  }
1109
1097
  const flowId = signal.flowId;
1110
- const sourceFingerprint = trimMaybeString(event?.event_id) ?? `${senderId}:${event.type}:${flowId ?? "none"}`;
1098
+ const sourceFingerprint = normalizeNullableString(event?.event_id) ?? `${senderId}:${event.type}:${flowId ?? "none"}`;
1111
1099
  if (!await (await loadMatrixDirectRoomDeps()).isStrictDirectRoom({
1112
1100
  client: params.client,
1113
1101
  roomId,
@@ -1413,246 +1401,6 @@ function resolveMatrixAckReactionConfig(params) {
1413
1401
  };
1414
1402
  }
1415
1403
  //#endregion
1416
- //#region extensions/matrix/src/matrix/monitor/location.ts
1417
- function decodeGeoUriParamValue(value) {
1418
- try {
1419
- return decodeURIComponent(value);
1420
- } catch {
1421
- return value;
1422
- }
1423
- }
1424
- function parseGeoUri(value) {
1425
- const trimmed = value.trim();
1426
- if (!trimmed) return null;
1427
- if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("geo:")) return null;
1428
- const [coordsPart, ...paramParts] = trimmed.slice(4).split(";");
1429
- if (!coordsPart) return null;
1430
- const coords = coordsPart.split(",");
1431
- if (coords.length < 2) return null;
1432
- const latitude = parseStrictFiniteNumber(coords[0] ?? "");
1433
- const longitude = parseStrictFiniteNumber(coords[1] ?? "");
1434
- if (latitude === void 0 || longitude === void 0) return null;
1435
- const params = /* @__PURE__ */ new Map();
1436
- for (const part of paramParts) {
1437
- const segment = part.trim();
1438
- if (!segment) continue;
1439
- const eqIndex = segment.indexOf("=");
1440
- const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
1441
- const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1);
1442
- const key = normalizeLowercaseStringOrEmpty(rawKey);
1443
- if (!key) continue;
1444
- const valuePart = rawValue.trim();
1445
- params.set(key, valuePart ? decodeGeoUriParamValue(valuePart) : "");
1446
- }
1447
- const accuracyRaw = params.get("u");
1448
- return {
1449
- latitude,
1450
- longitude,
1451
- accuracy: accuracyRaw ? parseStrictFiniteNumber(accuracyRaw) : void 0
1452
- };
1453
- }
1454
- function resolveMatrixLocation(params) {
1455
- const { eventType, content } = params;
1456
- if (!(eventType === EventType.Location || eventType === EventType.RoomMessage && content.msgtype === EventType.Location)) return null;
1457
- const geoUri = normalizeOptionalString(content.geo_uri) ?? "";
1458
- if (!geoUri) return null;
1459
- const parsed = parseGeoUri(geoUri);
1460
- if (!parsed) return null;
1461
- const caption = normalizeOptionalString(content.body) ?? "";
1462
- const location = {
1463
- latitude: parsed.latitude,
1464
- longitude: parsed.longitude,
1465
- accuracy: parsed.accuracy,
1466
- caption: caption || void 0,
1467
- source: "pin",
1468
- isLive: false
1469
- };
1470
- return {
1471
- text: formatLocationText(location),
1472
- context: toLocationContext(location)
1473
- };
1474
- }
1475
- //#endregion
1476
- //#region extensions/matrix/src/matrix/monitor/media.ts
1477
- const MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS = 3e4;
1478
- async function fetchMatrixMediaBuffer(params) {
1479
- try {
1480
- return { buffer: await params.client.downloadContent(params.mxcUrl, {
1481
- maxBytes: params.maxBytes,
1482
- readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS
1483
- }) };
1484
- } catch (err) {
1485
- if (isMatrixMediaSizeLimitError(err)) throw err;
1486
- throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err });
1487
- }
1488
- }
1489
- /**
1490
- * Download and decrypt encrypted media from a Matrix room.
1491
- * Uses the Matrix crypto adapter's decryptMedia helper.
1492
- */
1493
- async function fetchEncryptedMediaBuffer(params) {
1494
- if (!params.client.crypto) throw new Error("Cannot decrypt media: crypto not enabled");
1495
- const decrypted = await params.client.crypto.decryptMedia(params.file, {
1496
- maxBytes: params.maxBytes,
1497
- readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS
1498
- });
1499
- if (decrypted.byteLength > params.maxBytes) throw new MatrixMediaSizeLimitError();
1500
- return { buffer: decrypted };
1501
- }
1502
- async function downloadMatrixMedia(params) {
1503
- let fetched;
1504
- if (typeof params.sizeBytes === "number" && params.sizeBytes > params.maxBytes) throw new MatrixMediaSizeLimitError();
1505
- if (params.file) fetched = await fetchEncryptedMediaBuffer({
1506
- client: params.client,
1507
- file: params.file,
1508
- maxBytes: params.maxBytes
1509
- });
1510
- else fetched = await fetchMatrixMediaBuffer({
1511
- client: params.client,
1512
- mxcUrl: params.mxcUrl,
1513
- maxBytes: params.maxBytes
1514
- });
1515
- if (!fetched) return null;
1516
- const headerType = params.contentType ?? void 0;
1517
- const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(fetched.buffer, headerType, "inbound", params.maxBytes, params.originalFilename);
1518
- return {
1519
- path: saved.path,
1520
- contentType: saved.contentType,
1521
- placeholder: "[matrix media]"
1522
- };
1523
- }
1524
- //#endregion
1525
- //#region extensions/matrix/src/matrix/monitor/mentions.ts
1526
- const MATRIX_HTML_ENTITY_RE = /&(?:#x?[0-9a-f]+|amp|apos|gt|lt|nbsp|quot);/gi;
1527
- function decodeVisibleHtmlEntities(value) {
1528
- return value.replace(MATRIX_HTML_ENTITY_RE, (entity) => {
1529
- const decoded = decodeHtmlEntities(entity.startsWith("&#") ? entity : entity.toLowerCase());
1530
- return decoded === "\xA0" ? " " : decoded;
1531
- });
1532
- }
1533
- function normalizeVisibleMentionText(value) {
1534
- return normalizeLowercaseStringOrEmpty(decodeVisibleHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")).replace(/\s+/g, " "));
1535
- }
1536
- function extractVisibleMentionText(value) {
1537
- return normalizeVisibleMentionText(value ?? "");
1538
- }
1539
- function resolveMatrixUserLocalpart(userId) {
1540
- const trimmed = userId.trim();
1541
- if (!trimmed.startsWith("@")) return null;
1542
- const colonIndex = trimmed.indexOf(":");
1543
- if (colonIndex <= 1) return null;
1544
- return trimmed.slice(1, colonIndex).trim() || null;
1545
- }
1546
- function resolveMatrixMentionPrefixCandidates(params) {
1547
- const candidates = [];
1548
- const seen = /* @__PURE__ */ new Set();
1549
- const append = (candidate) => {
1550
- const trimmed = candidate?.trim();
1551
- if (!trimmed) return;
1552
- const normalized = normalizeLowercaseStringOrEmpty(trimmed);
1553
- if (seen.has(normalized)) return;
1554
- seen.add(normalized);
1555
- candidates.push(trimmed);
1556
- };
1557
- append(params.userId);
1558
- const localpart = params.userId ? resolveMatrixUserLocalpart(params.userId) : null;
1559
- append(localpart ? `@${localpart}` : null);
1560
- append(params.displayName);
1561
- append(params.displayName ? `@${params.displayName}` : null);
1562
- append(params.displayName ? `@[${params.displayName}]` : null);
1563
- return candidates;
1564
- }
1565
- function stripMatchedMatrixMentionPrefix(text, pattern) {
1566
- const match = text.match(pattern);
1567
- if (!match) return null;
1568
- return text.slice(match[0].length).trimStart();
1569
- }
1570
- function stripNativeMatrixMentionPrefix(text, candidate) {
1571
- return stripMatchedMatrixMentionPrefix(text, new RegExp(`^\\s*${escapeRegExp(candidate)}(?:\\s*[:,])?(?:\\s+|$)`, "i"));
1572
- }
1573
- function stripRegexMatrixMentionPrefix(text, pattern) {
1574
- const flags = pattern.flags.replace(/[gy]/g, "");
1575
- return stripMatchedMatrixMentionPrefix(text, new RegExp(`^\\s*(?:${pattern.source})(?:\\s*[:,])?(?:\\s+|$)`, flags));
1576
- }
1577
- function stripMatrixMentionPrefix(params) {
1578
- const text = params.text;
1579
- if (!text) return text;
1580
- for (const candidate of resolveMatrixMentionPrefixCandidates(params)) {
1581
- const stripped = stripNativeMatrixMentionPrefix(text, candidate);
1582
- if (stripped !== null) return stripped;
1583
- }
1584
- for (const pattern of params.mentionRegexes ?? []) {
1585
- const stripped = stripRegexMatrixMentionPrefix(text, pattern);
1586
- if (stripped !== null) return stripped;
1587
- }
1588
- return text;
1589
- }
1590
- function isVisibleMentionLabel(params) {
1591
- const cleaned = extractVisibleMentionText(params.text);
1592
- if (!cleaned) return false;
1593
- if (params.mentionRegexes.some((pattern) => pattern.test(cleaned))) return true;
1594
- const localpart = resolveMatrixUserLocalpart(params.userId);
1595
- return [
1596
- extractVisibleMentionText(params.userId),
1597
- localpart ? extractVisibleMentionText(localpart) : null,
1598
- localpart ? extractVisibleMentionText(`@${localpart}`) : null,
1599
- params.displayName ? extractVisibleMentionText(params.displayName) : null,
1600
- params.displayName ? extractVisibleMentionText(`@${params.displayName}`) : null,
1601
- params.displayName ? extractVisibleMentionText(`@[${params.displayName}]`) : null
1602
- ].filter((value) => Boolean(value)).includes(cleaned);
1603
- }
1604
- function hasVisibleRoomMention(value) {
1605
- const cleaned = extractVisibleMentionText(value);
1606
- return /(^|[^a-z0-9_])@room\b/i.test(cleaned);
1607
- }
1608
- /**
1609
- * Check if formatted_body contains a matrix.to link whose visible label still
1610
- * looks like a real mention for the given user. Do not trust href alone, since
1611
- * senders can hide arbitrary matrix.to links behind unrelated link text.
1612
- * Many Matrix clients (including Element) use HTML links in formatted_body instead of
1613
- * or in addition to the m.mentions field.
1614
- */
1615
- function checkFormattedBodyMention(params) {
1616
- if (!params.formattedBody || !params.userId) return false;
1617
- for (const match of params.formattedBody.matchAll(/<a\b[^>]*href=(["'])(https:\/\/matrix\.to\/#[^"']+)\1[^>]*>(.*?)<\/a>/gis)) {
1618
- const href = match[2];
1619
- const visibleLabel = match[3] ?? "";
1620
- if (!href) continue;
1621
- try {
1622
- const parsed = new URL(href);
1623
- if (decodeURIComponent(parsed.hash.replace(/^#\/?/, "").trim()) !== params.userId.trim()) continue;
1624
- if (isVisibleMentionLabel({
1625
- text: visibleLabel,
1626
- userId: params.userId,
1627
- mentionRegexes: params.mentionRegexes,
1628
- displayName: params.displayName
1629
- })) return true;
1630
- } catch {
1631
- continue;
1632
- }
1633
- }
1634
- return false;
1635
- }
1636
- function resolveMentions(params) {
1637
- const mentions = params.content["m.mentions"];
1638
- const mentionedUsers = Array.isArray(mentions?.user_ids) ? new Set(mentions.user_ids) : /* @__PURE__ */ new Set();
1639
- const textMentioned = getMatrixRuntime().channel.mentions.matchesMentionPatterns(params.text ?? "", params.mentionRegexes);
1640
- const visibleRoomMention = hasVisibleRoomMention(params.text) || hasVisibleRoomMention(params.content.formatted_body);
1641
- const mentionedInFormattedBody = params.userId ? checkFormattedBodyMention({
1642
- formattedBody: params.content.formatted_body,
1643
- userId: params.userId,
1644
- displayName: params.displayName,
1645
- mentionRegexes: params.mentionRegexes
1646
- }) : false;
1647
- const metadataBackedUserMention = Boolean(params.userId && mentionedUsers.has(params.userId) && (mentionedInFormattedBody || textMentioned));
1648
- const metadataBackedRoomMention = Boolean(mentions?.room) && visibleRoomMention;
1649
- const explicitMention = mentionedInFormattedBody || metadataBackedUserMention || metadataBackedRoomMention;
1650
- return {
1651
- wasMentioned: explicitMention || textMentioned || visibleRoomMention,
1652
- hasExplicitMention: explicitMention
1653
- };
1654
- }
1655
- //#endregion
1656
1404
  //#region extensions/matrix/src/matrix/monitor/preflight-audio.ts
1657
1405
  const MATRIX_DEFAULT_ECHO_TRANSCRIPT_FORMAT = "📝 \"{transcript}\"";
1658
1406
  const loadMatrixPreflightAudioRuntime = createLazyRuntimeModule(() => import("./preflight-audio.runtime-BZxxQsxY.js"));
@@ -1691,8 +1439,10 @@ async function resolveMatrixPreflightAudioTranscript(params) {
1691
1439
  if (params.abortSignal?.aborted) return;
1692
1440
  const transcript = await transcribeFirstAudio({
1693
1441
  ctx: {
1694
- MediaPaths: [params.mediaPath],
1695
- MediaTypes: params.mediaContentType ? [params.mediaContentType] : void 0,
1442
+ media: [{
1443
+ path: params.mediaPath,
1444
+ contentType: params.mediaContentType
1445
+ }],
1696
1446
  Provider: "matrix",
1697
1447
  Surface: "matrix",
1698
1448
  OriginatingChannel: "matrix",
@@ -1732,102 +1482,134 @@ async function sendMatrixPreflightAudioTranscriptEcho(params) {
1732
1482
  }
1733
1483
  }
1734
1484
  //#endregion
1735
- //#region extensions/matrix/src/matrix/monitor/replies.ts
1736
- const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
1737
- const THINKING_BLOCK_RE = /<\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
1738
- function shouldSuppressReasoningReplyText(text) {
1739
- if (typeof text !== "string") return false;
1740
- const trimmedStart = text.trimStart();
1741
- if (!trimmedStart) return false;
1742
- if (normalizeLowercaseStringOrEmpty(trimmedStart).startsWith("reasoning:")) return true;
1743
- THINKING_TAG_RE.lastIndex = 0;
1744
- if (!THINKING_TAG_RE.test(text)) return false;
1745
- THINKING_BLOCK_RE.lastIndex = 0;
1746
- const withoutThinkingBlocks = text.replace(THINKING_BLOCK_RE, "");
1747
- THINKING_TAG_RE.lastIndex = 0;
1748
- return !withoutThinkingBlocks.replace(THINKING_TAG_RE, "").trim();
1485
+ //#region extensions/matrix/src/matrix/monitor/handler-helpers.ts
1486
+ const MATRIX_TOOL_PROGRESS_MAX_CHARS = 300;
1487
+ const MAX_TRACKED_SHARED_DM_CONTEXT_NOTICES = 512;
1488
+ function resolveMatrixMentionPrecheckText(params) {
1489
+ if (params.locationText?.trim()) return params.locationText.trim();
1490
+ if (typeof params.content.body === "string" && params.content.body.trim()) return params.content.body.trim();
1491
+ if (isPollStartType(params.eventType)) {
1492
+ const parsed = parsePollStartContent(params.content);
1493
+ if (parsed) return formatPollAsText(parsed);
1494
+ }
1495
+ return "";
1749
1496
  }
1750
- async function deliverMatrixReplies(params) {
1751
- const core = getMatrixRuntime();
1752
- const tableMode = params.tableMode ?? core.channel.text.resolveMarkdownTableMode({
1753
- cfg: params.cfg,
1754
- channel: "matrix",
1755
- accountId: params.accountId
1497
+ function hasBundledMatrixReplacementRelation(event) {
1498
+ const relations = event.unsigned?.["m.relations"];
1499
+ if (!relations || typeof relations !== "object") return false;
1500
+ return relations[RelationType.Replace] !== void 0;
1501
+ }
1502
+ function resolveMatrixInboundBodyText(params) {
1503
+ if (params.mediaPlaceholder) return params.rawBody || params.mediaPlaceholder;
1504
+ if (!params.mediaDownloadFailed || !params.hadMediaUrl) return params.rawBody;
1505
+ if (params.mediaSizeLimitExceeded) return formatMatrixMediaTooLargeText({
1506
+ body: params.rawBody,
1507
+ filename: params.filename,
1508
+ msgtype: params.msgtype
1756
1509
  });
1757
- const logVerbose = (message) => {
1758
- if (core.logging.shouldLogVerbose()) params.runtime.log?.(message);
1759
- };
1760
- let hasReplied = false;
1761
- let deliveredAny = false;
1762
- for (const reply of params.replies) {
1763
- if (reply.isReasoning === true || shouldSuppressReasoningReplyText(reply.text)) {
1764
- logVerbose("matrix reply suppressed as reasoning-only");
1765
- continue;
1766
- }
1767
- const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
1768
- if (!reply?.text && !hasMedia) {
1769
- if (reply?.audioAsVoice) {
1770
- logVerbose("matrix reply has audioAsVoice without media/text; skipping");
1771
- continue;
1772
- }
1773
- params.runtime.error?.("matrix reply missing text/media");
1774
- continue;
1775
- }
1776
- const replyToIdRaw = (reply.replyToId ?? params.replyToId)?.trim();
1777
- const replyToId = params.threadId ? replyToIdRaw : params.replyToMode === "off" ? void 0 : replyToIdRaw;
1778
- const rawText = reply.text ?? "";
1779
- const mediaList = reply.mediaUrls?.length ? reply.mediaUrls : reply.mediaUrl ? [reply.mediaUrl] : [];
1780
- const shouldIncludeReply = (id) => Boolean(id) && (params.threadId || params.replyToMode === "all" || !hasReplied);
1781
- const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : void 0;
1782
- if (mediaList.length === 0) {
1783
- let sentTextChunk = false;
1784
- const { chunks } = chunkMatrixText(rawText, {
1785
- cfg: params.cfg,
1786
- accountId: params.accountId,
1787
- tableMode
1788
- });
1789
- for (const chunk of chunks) {
1790
- const trimmed = chunk.trim();
1791
- if (!trimmed) continue;
1792
- await sendMessageMatrix(params.roomId, trimmed, {
1793
- client: params.client,
1794
- cfg: params.cfg,
1795
- replyToId: replyToIdForReply,
1796
- threadId: params.threadId,
1797
- accountId: params.accountId
1798
- });
1799
- deliveredAny = true;
1800
- sentTextChunk = true;
1801
- }
1802
- if (replyToIdForReply && !hasReplied && sentTextChunk) hasReplied = true;
1803
- continue;
1804
- }
1805
- let first = true;
1806
- for (const mediaUrl of mediaList) {
1807
- const caption = first ? rawText : "";
1808
- await sendMessageMatrix(params.roomId, caption, {
1809
- client: params.client,
1810
- cfg: params.cfg,
1811
- mediaUrl,
1812
- mediaLocalRoots: params.mediaLocalRoots,
1813
- replyToId: replyToIdForReply,
1814
- threadId: params.threadId,
1815
- audioAsVoice: reply.audioAsVoice,
1816
- accountId: params.accountId
1817
- });
1818
- deliveredAny = true;
1819
- first = false;
1820
- }
1821
- if (replyToIdForReply && !hasReplied) hasReplied = true;
1510
+ return formatMatrixMediaUnavailableText({
1511
+ body: params.rawBody,
1512
+ filename: params.filename,
1513
+ msgtype: params.msgtype
1514
+ });
1515
+ }
1516
+ function markTrackedRoomIfFirst(set, roomId) {
1517
+ if (set.has(roomId)) return false;
1518
+ set.add(roomId);
1519
+ if (set.size > MAX_TRACKED_SHARED_DM_CONTEXT_NOTICES) {
1520
+ const oldest = set.keys().next().value;
1521
+ if (typeof oldest === "string") set.delete(oldest);
1822
1522
  }
1823
- return deliveredAny;
1523
+ return true;
1524
+ }
1525
+ function resolveMatrixSharedDmContextNotice(params) {
1526
+ if ((params.dmSessionScope ?? "per-user") === "per-room") return null;
1527
+ if (params.sentRooms.has(params.roomId)) return null;
1528
+ try {
1529
+ const currentSession = resolveMatrixStoredSessionMeta(getSessionEntry({
1530
+ storePath: params.storePath,
1531
+ sessionKey: params.sessionKey
1532
+ }));
1533
+ if (!currentSession) return null;
1534
+ if (currentSession.channel && currentSession.channel !== "matrix") return null;
1535
+ if (currentSession.accountId && currentSession.accountId !== params.accountId) return null;
1536
+ if (!currentSession.directUserId) return null;
1537
+ if (!currentSession.roomId || currentSession.roomId === params.roomId) return null;
1538
+ return [
1539
+ "This Matrix DM is sharing a session with another Matrix DM room.",
1540
+ "Use /focus here for a one-off isolated thread session when thread bindings are enabled, or set",
1541
+ "channels.matrix.dm.sessionScope to per-room to isolate each Matrix DM room."
1542
+ ].join(" ");
1543
+ } catch (err) {
1544
+ params.logVerboseMessage(`matrix: failed checking shared DM session notice room=${params.roomId} (${String(err)})`);
1545
+ return null;
1546
+ }
1547
+ }
1548
+ function resolveMatrixPendingHistoryText(params) {
1549
+ if (params.mentionPrecheckText) return params.mentionPrecheckText;
1550
+ if (!params.mediaUrl) return "";
1551
+ const body = typeof params.content.body === "string" ? params.content.body.trim() : void 0;
1552
+ const filename = typeof params.content.filename === "string" ? params.content.filename.trim() : void 0;
1553
+ const msgtype = typeof params.content.msgtype === "string" ? params.content.msgtype : void 0;
1554
+ return formatMatrixMessageText({
1555
+ body: resolveMatrixMessageBody({
1556
+ body,
1557
+ filename,
1558
+ msgtype
1559
+ }),
1560
+ attachment: resolveMatrixMessageAttachment({
1561
+ body,
1562
+ filename,
1563
+ msgtype
1564
+ })
1565
+ }) ?? "";
1566
+ }
1567
+ function isMatrixAudioMediaEnabled(cfg) {
1568
+ return cfg.tools?.media?.audio?.enabled !== false;
1569
+ }
1570
+ function shouldDeferMatrixAudioPreflightForRoomIngress(params) {
1571
+ if (!isMatrixAudioMediaEnabled(params.cfg)) return false;
1572
+ const content = params.content;
1573
+ const contentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
1574
+ const contentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
1575
+ const mediaUrl = contentUrl ?? contentFile?.url;
1576
+ const contentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
1577
+ return mediaUrl?.startsWith("mxc://") === true && isMatrixAudioContent({
1578
+ msgtype: typeof content.msgtype === "string" ? content.msgtype : void 0,
1579
+ mimetype: contentInfo?.mimetype
1580
+ });
1581
+ }
1582
+ function resolveMatrixAllowBotsMode(value) {
1583
+ if (value === true) return "all";
1584
+ if (value === "mentions") return "mentions";
1585
+ return "off";
1586
+ }
1587
+ function formatMatrixToolProgressMarkdownCode(text) {
1588
+ return `\`${(text.length <= MATRIX_TOOL_PROGRESS_MAX_CHARS ? text : `${truncateUtf16Safe(text, MATRIX_TOOL_PROGRESS_MAX_CHARS - 1).trimEnd()}...`).replaceAll("`", "'")}\``;
1824
1589
  }
1825
1590
  //#endregion
1826
- //#region extensions/matrix/src/matrix/monitor/context-summary.ts
1827
- function trimMatrixMaybeString(value) {
1828
- if (typeof value !== "string") return;
1829
- return value.trim() || void 0;
1591
+ //#region extensions/matrix/src/matrix/monitor/handler-runtime.ts
1592
+ async function redactMatrixDraftEvent(client, roomId, draftEventId) {
1593
+ await client.redactEvent(roomId, draftEventId).catch(() => {});
1594
+ }
1595
+ function buildMatrixFinalizedPreviewContent() {
1596
+ return { [MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY]: true };
1597
+ }
1598
+ const loadMatrixSendModule = createLazyRuntimeModule(() => import("./send-C9Ssx-iV.js").then((n) => n.s));
1599
+ const loadAcpBindingRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/acp-binding-runtime"));
1600
+ const loadSessionBindingRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/session-binding-runtime"));
1601
+ const loadMatrixReactionEvents = createLazyRuntimeModule(() => import("./reaction-events-DrK9Zua7.js"));
1602
+ const loadMatrixDraftStream = createLazyRuntimeModule(() => import("./draft-stream-CIjafRr0.js"));
1603
+ async function matrixTextWouldActivateMentions(client, text) {
1604
+ const { resolveMatrixMentionsForBody } = await loadMatrixSendModule();
1605
+ const mentions = await resolveMatrixMentionsForBody({
1606
+ client,
1607
+ body: text
1608
+ });
1609
+ return mentions.room === true || (mentions.user_ids?.length ?? 0) > 0;
1830
1610
  }
1611
+ //#endregion
1612
+ //#region extensions/matrix/src/matrix/monitor/context-summary.ts
1831
1613
  function summarizeMatrixMessageContextEvent(event) {
1832
1614
  if (isPollStartType(event.type)) {
1833
1615
  const pollSummary = parsePollStartContent(event.content);
@@ -1836,14 +1618,14 @@ function summarizeMatrixMessageContextEvent(event) {
1836
1618
  const content = event.content;
1837
1619
  return formatMatrixMessageText({
1838
1620
  body: resolveMatrixMessageBody({
1839
- body: trimMatrixMaybeString(content.body),
1840
- filename: trimMatrixMaybeString(content.filename),
1841
- msgtype: trimMatrixMaybeString(content.msgtype)
1621
+ body: normalizeOptionalString(content.body),
1622
+ filename: normalizeOptionalString(content.filename),
1623
+ msgtype: normalizeOptionalString(content.msgtype)
1842
1624
  }),
1843
1625
  attachment: resolveMatrixMessageAttachment({
1844
- body: trimMatrixMaybeString(content.body),
1845
- filename: trimMatrixMaybeString(content.filename),
1846
- msgtype: trimMatrixMaybeString(content.msgtype)
1626
+ body: normalizeOptionalString(content.body),
1627
+ filename: normalizeOptionalString(content.filename),
1628
+ msgtype: normalizeOptionalString(content.msgtype)
1847
1629
  })
1848
1630
  });
1849
1631
  }
@@ -1891,7 +1673,7 @@ function createMatrixReplyContextResolver(params) {
1891
1673
  if (rawEvent.unsigned?.redacted_because) return remember(cacheKey, {});
1892
1674
  const replyToBody = summarizeMatrixReplyEvent(rawEvent);
1893
1675
  if (!replyToBody) return remember(cacheKey, {});
1894
- const senderId = trimMatrixMaybeString(rawEvent.sender);
1676
+ const senderId = normalizeOptionalString(rawEvent.sender);
1895
1677
  const senderName = senderId && await params.getMemberDisplayName(input.roomId, senderId).catch(() => void 0);
1896
1678
  return remember(cacheKey, {
1897
1679
  replyToBody,
@@ -1901,35 +1683,540 @@ function createMatrixReplyContextResolver(params) {
1901
1683
  };
1902
1684
  }
1903
1685
  //#endregion
1904
- //#region extensions/matrix/src/matrix/monitor/room-history.ts
1905
- /** Maximum entries retained per room (hard cap to bound memory). */
1906
- const DEFAULT_MAX_QUEUE_SIZE = 200;
1907
- /** Maximum number of rooms to retain queues for (FIFO eviction beyond this). */
1908
- const DEFAULT_MAX_ROOM_QUEUES = 1e3;
1909
- /** Maximum number of (agentId, roomId) watermark entries to retain. */
1910
- const MAX_WATERMARK_ENTRIES = 5e3;
1911
- /** Maximum prepared trigger snapshots retained per room for retry reuse. */
1912
- const MAX_PREPARED_TRIGGER_ENTRIES = 500;
1913
- /** Maximum thread queues retained per room (FIFO eviction beyond this). */
1914
- const MAX_THREAD_QUEUES_PER_ROOM = 200;
1915
- function createRoomHistoryTrackerInternal(maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES, maxWatermarkEntries = MAX_WATERMARK_ENTRIES, maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES) {
1916
- const roomQueues = /* @__PURE__ */ new Map();
1917
- /** Maps `{agentId, roomId, scope}` → absolute consumed-up-to index */
1918
- const agentWatermarks = /* @__PURE__ */ new Map();
1919
- let nextQueueGeneration = 1;
1920
- function clearRoomWatermarks(roomId) {
1921
- for (const key of agentWatermarks.keys()) if (JSON.parse(key)?.roomId === roomId) agentWatermarks.delete(key);
1922
- }
1923
- function clearThreadWatermarks(roomId, threadRootId) {
1924
- for (const key of agentWatermarks.keys()) {
1925
- const parsed = JSON.parse(key);
1926
- if (parsed?.roomId === roomId && parsed.scope === threadRootId) agentWatermarks.delete(key);
1686
+ //#region extensions/matrix/src/matrix/monitor/thread-context.ts
1687
+ const MAX_TRACKED_THREAD_STARTERS = 256;
1688
+ const MAX_THREAD_STARTER_BODY_LENGTH = 500;
1689
+ function truncateThreadStarterBody(value) {
1690
+ if (value.length <= MAX_THREAD_STARTER_BODY_LENGTH) return value;
1691
+ return `${sliceUtf16Safe(value, 0, MAX_THREAD_STARTER_BODY_LENGTH - 3)}...`;
1692
+ }
1693
+ function summarizeMatrixThreadStarterEvent(event) {
1694
+ const body = summarizeMatrixMessageContextEvent(event);
1695
+ if (body) return truncateThreadStarterBody(body);
1696
+ const content = event.content;
1697
+ const msgtype = normalizeOptionalString(content.msgtype);
1698
+ if (msgtype) return `Matrix ${msgtype} message`;
1699
+ const eventType = normalizeOptionalString(event.type);
1700
+ return eventType ? `Matrix ${eventType} event` : void 0;
1701
+ }
1702
+ function formatMatrixThreadStarterBody(params) {
1703
+ const senderLabel = params.senderName ?? params.senderId ?? "unknown sender";
1704
+ const lines = [`Matrix thread root ${params.threadRootId} from ${senderLabel}:`];
1705
+ if (params.summary) lines.push(params.summary);
1706
+ return lines.join("\n");
1707
+ }
1708
+ function createMatrixThreadContextResolver(params) {
1709
+ const cache = /* @__PURE__ */ new Map();
1710
+ const remember = (key, value) => {
1711
+ cache.set(key, value);
1712
+ if (cache.size > MAX_TRACKED_THREAD_STARTERS) {
1713
+ const oldest = cache.keys().next().value;
1714
+ if (typeof oldest === "string") cache.delete(oldest);
1927
1715
  }
1928
- }
1929
- function createHistoryQueue() {
1930
- return {
1931
- entries: [],
1932
- baseIndex: 0,
1716
+ return value;
1717
+ };
1718
+ return async (input) => {
1719
+ const cacheKey = `${input.roomId}:${input.threadRootId}`;
1720
+ const cached = cache.get(cacheKey);
1721
+ if (cached) return cached;
1722
+ const rootEvent = await params.client.getEvent(input.roomId, input.threadRootId).catch((err) => {
1723
+ params.logVerboseMessage(`matrix: failed resolving thread root room=${input.roomId} id=${input.threadRootId}: ${String(err)}`);
1724
+ return null;
1725
+ });
1726
+ if (!rootEvent) return { threadStarterBody: `Matrix thread root ${input.threadRootId}` };
1727
+ const rawEvent = rootEvent;
1728
+ const senderId = normalizeOptionalString(rawEvent.sender);
1729
+ const senderName = senderId && await params.getMemberDisplayName(input.roomId, senderId).catch(() => void 0);
1730
+ const senderLabel = senderName ?? senderId;
1731
+ const summary = summarizeMatrixThreadStarterEvent(rawEvent);
1732
+ return remember(cacheKey, {
1733
+ threadStarterBody: formatMatrixThreadStarterBody({
1734
+ threadRootId: input.threadRootId,
1735
+ senderId,
1736
+ senderName,
1737
+ summary
1738
+ }),
1739
+ senderId,
1740
+ senderLabel,
1741
+ summary
1742
+ });
1743
+ };
1744
+ }
1745
+ //#endregion
1746
+ //#region extensions/matrix/src/matrix/monitor/handler-context.ts
1747
+ async function resolveMatrixInboundContext(config) {
1748
+ const { client, core, cfg, accountId, runtime, logVerboseMessage, roomId, event, eventTs, route: _route, isDirectMessage, isRoom, effectiveRoomUsers, groupPolicy, effectiveGroupAllowFrom, contextVisibilityMode, resolveThreadContext, resolveReplyContext, threadRootId, thread, getRoomInfo, senderId, senderName, bodyText, commandBodyText, roomConfig, messageId, inboundHistory, wasMentioned, effectiveWasMentioned, shouldBypassMention, canDetectMention, shouldRequireMention, commandAuthorized, locationPayload, media, preflightAudioTranscript, historyLimit, hasExplicitSessionBinding, dmSessionScope, sharedDmContextNoticeRooms, resolveStorePath: resolveStorePathImpl, createChannelInboundEnvelopeBuilder: createChannelInboundEnvelopeBuilderImpl, finalizeInboundContext } = config;
1749
+ const replyToEventId = resolveMatrixReplyToEventId(event.content);
1750
+ const threadTarget = thread.threadId;
1751
+ const isRoomContextSenderAllowed = (contextSenderId) => {
1752
+ if (!isRoom || !contextSenderId) return true;
1753
+ if (effectiveRoomUsers.length > 0) return resolveMatrixAllowListMatch({
1754
+ allowList: effectiveRoomUsers,
1755
+ userId: contextSenderId
1756
+ }).allowed;
1757
+ if (groupPolicy === "allowlist" && effectiveGroupAllowFrom.length > 0) return resolveMatrixAllowListMatch({
1758
+ allowList: effectiveGroupAllowFrom,
1759
+ userId: contextSenderId
1760
+ }).allowed;
1761
+ return true;
1762
+ };
1763
+ const shouldIncludeRoomContextSender = (kind, contextSenderId) => evaluateSupplementalContextVisibility({
1764
+ mode: contextVisibilityMode,
1765
+ kind,
1766
+ senderAllowed: isRoomContextSenderAllowed(contextSenderId)
1767
+ }).include;
1768
+ let threadContext = threadRootId ? await resolveThreadContext({
1769
+ roomId,
1770
+ threadRootId
1771
+ }) : void 0;
1772
+ let threadContextBlockedByPolicy = false;
1773
+ if (threadContext?.senderId && !shouldIncludeRoomContextSender("thread", threadContext.senderId)) {
1774
+ logVerboseMessage(`matrix: drop thread root context (mode=${contextVisibilityMode})`);
1775
+ threadContextBlockedByPolicy = true;
1776
+ threadContext = void 0;
1777
+ }
1778
+ let replyContext;
1779
+ if (replyToEventId && replyToEventId === threadRootId && threadContext?.summary) replyContext = {
1780
+ replyToBody: threadContext.summary,
1781
+ replyToSender: threadContext.senderLabel,
1782
+ replyToSenderId: threadContext.senderId
1783
+ };
1784
+ else if (replyToEventId && replyToEventId === threadRootId && threadContextBlockedByPolicy) replyContext = await resolveReplyContext({
1785
+ roomId,
1786
+ eventId: replyToEventId
1787
+ });
1788
+ else replyContext = replyToEventId ? await resolveReplyContext({
1789
+ roomId,
1790
+ eventId: replyToEventId
1791
+ }) : void 0;
1792
+ const replySenderAllowed = !replyContext?.replyToSenderId || isRoomContextSenderAllowed(replyContext.replyToSenderId);
1793
+ const roomName = (isRoom ? await getRoomInfo(roomId) : void 0)?.name;
1794
+ const envelopeFrom = isDirectMessage ? senderName : roomName ?? roomId;
1795
+ const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
1796
+ const storePath = resolveStorePathImpl(cfg.session?.store, { agentId: _route.agentId });
1797
+ const buildEnvelope = createChannelInboundEnvelopeBuilderImpl({
1798
+ cfg,
1799
+ route: _route
1800
+ });
1801
+ const sharedDmNoticeSessionKey = threadTarget ? _route.mainSessionKey || _route.sessionKey : _route.sessionKey;
1802
+ const sharedDmContextNotice = isDirectMessage ? hasExplicitSessionBinding ? null : resolveMatrixSharedDmContextNotice({
1803
+ storePath,
1804
+ sessionKey: sharedDmNoticeSessionKey,
1805
+ roomId,
1806
+ accountId: _route.accountId,
1807
+ dmSessionScope,
1808
+ sentRooms: sharedDmContextNoticeRooms,
1809
+ logVerboseMessage
1810
+ }) : null;
1811
+ const body = buildEnvelope({
1812
+ channel: "Matrix",
1813
+ from: envelopeFrom,
1814
+ timestamp: eventTs ?? void 0,
1815
+ body: textWithId
1816
+ });
1817
+ const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
1818
+ const quoteHidden = Boolean(replyContext && !evaluateSupplementalContextVisibility({
1819
+ mode: contextVisibilityMode,
1820
+ kind: "quote",
1821
+ senderAllowed: replySenderAllowed
1822
+ }).include);
1823
+ const ctxPayload = buildChannelInboundEventContext({
1824
+ channel: "matrix",
1825
+ contextVisibility: contextVisibilityMode,
1826
+ finalize: finalizeInboundContext,
1827
+ supplemental: {
1828
+ quote: replyContext ? {
1829
+ id: threadTarget ? void 0 : replyToEventId ?? void 0,
1830
+ body: replyContext.replyToBody,
1831
+ sender: replyContext.replyToSender,
1832
+ senderAllowed: replySenderAllowed
1833
+ } : void 0,
1834
+ thread: {
1835
+ starterBody: threadContext?.threadStarterBody,
1836
+ senderAllowed: threadContext ? true : void 0
1837
+ },
1838
+ groupSystemPrompt: isRoom ? groupSystemPrompt : void 0
1839
+ },
1840
+ media: toInboundMediaFacts(media ? [{
1841
+ path: media.path,
1842
+ url: media.path,
1843
+ contentType: media.contentType,
1844
+ transcribed: preflightAudioTranscript !== void 0
1845
+ }] : void 0),
1846
+ messageId,
1847
+ timestamp: eventTs ?? void 0,
1848
+ from: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
1849
+ sender: {
1850
+ id: senderId,
1851
+ name: senderName,
1852
+ username: senderId.split(":")[0]?.replace(/^@/, "")
1853
+ },
1854
+ conversation: {
1855
+ kind: isDirectMessage ? "direct" : "channel",
1856
+ id: roomId,
1857
+ label: envelopeFrom,
1858
+ nativeChannelId: roomId,
1859
+ threadId: threadTarget
1860
+ },
1861
+ route: {
1862
+ agentId: _route.agentId,
1863
+ dmScope: _route.dmScope,
1864
+ accountId: _route.accountId,
1865
+ routeSessionKey: _route.sessionKey
1866
+ },
1867
+ reply: {
1868
+ to: `room:${roomId}`,
1869
+ replyToId: threadTarget ? void 0 : replyToEventId ?? void 0,
1870
+ messageThreadId: threadTarget,
1871
+ nativeChannelId: roomId
1872
+ },
1873
+ message: {
1874
+ body,
1875
+ rawBody: bodyText,
1876
+ commandBody: commandBodyText,
1877
+ bodyForAgent: bodyText,
1878
+ inboundHistory: inboundHistory && inboundHistory.length > 0 ? inboundHistory : void 0
1879
+ },
1880
+ sessionTranscript: { historyLimit: isRoom ? historyLimit : 0 },
1881
+ access: {
1882
+ ...isRoom ? { mentions: {
1883
+ canDetectMention: true,
1884
+ wasMentioned,
1885
+ requireMention: shouldRequireMention
1886
+ } } : {},
1887
+ commands: { authorized: commandAuthorized }
1888
+ },
1889
+ extra: {
1890
+ GroupSubject: isRoom ? roomName ?? roomId : void 0,
1891
+ GroupId: isRoom ? roomId : void 0,
1892
+ GroupChannel: isRoom ? roomId : void 0,
1893
+ ...locationPayload?.context,
1894
+ CommandSource: "text",
1895
+ NativeDirectUserId: isDirectMessage ? senderId : void 0
1896
+ }
1897
+ });
1898
+ if (quoteHidden) logVerboseMessage(`matrix: drop reply context (mode=${contextVisibilityMode})`);
1899
+ logVerboseMessage(`matrix inbound: room=${roomId} from=${senderId} preview="${truncateUtf16Safe(bodyText, 200).replace(/\n/g, "\\n")}"`);
1900
+ const replyTarget = ctxPayload.To;
1901
+ if (!replyTarget) {
1902
+ runtime.error?.("matrix: missing reply target");
1903
+ return null;
1904
+ }
1905
+ const { ackReaction, ackReactionScope: ackScope } = resolveMatrixAckReactionConfig({
1906
+ cfg,
1907
+ agentId: _route.agentId,
1908
+ accountId
1909
+ });
1910
+ const shouldAckReaction = () => Boolean(ackReaction && core.channel.reactions.shouldAckReaction({
1911
+ scope: ackScope,
1912
+ isDirect: isDirectMessage,
1913
+ isGroup: isRoom,
1914
+ isMentionableGroup: isRoom,
1915
+ requireMention: shouldRequireMention,
1916
+ canDetectMention,
1917
+ effectiveWasMentioned,
1918
+ shouldBypassMention
1919
+ }));
1920
+ if (shouldAckReaction() && messageId) loadMatrixSendModule().then(({ reactMatrixMessage }) => reactMatrixMessage(roomId, messageId, ackReaction, client)).catch((err) => {
1921
+ logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`);
1922
+ });
1923
+ if (messageId) loadMatrixSendModule().then(({ sendReadReceiptMatrix }) => sendReadReceiptMatrix(roomId, messageId, client)).catch((err) => {
1924
+ logVerboseMessage(`matrix: read receipt failed room=${roomId} id=${messageId}: ${String(err)}`);
1925
+ });
1926
+ return {
1927
+ replyToEventId,
1928
+ threadTarget,
1929
+ storePath,
1930
+ ctxPayload,
1931
+ replyTarget,
1932
+ sharedDmContextNotice
1933
+ };
1934
+ }
1935
+ //#endregion
1936
+ //#region extensions/matrix/src/matrix/monitor/handler-draft-controller.ts
1937
+ async function createMatrixDraftController(params) {
1938
+ const { streaming, previewToolProgressEnabled, replyToMode, messageId, threadTarget, accountConfig, cfg, accountId, roomId, client, logVerboseMessage } = params;
1939
+ let draftConsumed = false;
1940
+ const draftStreamingEnabled = streaming !== "off";
1941
+ const quietDraftStreaming = streaming === "quiet" || streaming === "progress";
1942
+ const progressDraftStreaming = streaming === "progress";
1943
+ const draftReplyToId = replyToMode !== "off" && !threadTarget ? messageId : void 0;
1944
+ const draftStream = draftStreamingEnabled ? await loadMatrixDraftStream().then(({ createMatrixDraftStream }) => createMatrixDraftStream({
1945
+ roomId,
1946
+ client,
1947
+ cfg,
1948
+ mode: quietDraftStreaming ? "quiet" : "partial",
1949
+ threadId: threadTarget,
1950
+ replyToId: draftReplyToId,
1951
+ preserveReplyId: replyToMode === "all",
1952
+ accountId,
1953
+ log: logVerboseMessage
1954
+ })) : void 0;
1955
+ const shouldStreamPreviewToolProgress = Boolean(draftStream) && previewToolProgressEnabled;
1956
+ const shouldSuppressDefaultToolProgressMessages = Boolean(draftStream) && (shouldStreamPreviewToolProgress || params.streaming === "progress");
1957
+ let currentDraftMessageGeneration = 0;
1958
+ let currentDraftBlockOffset = 0;
1959
+ let latestDraftFullText = "";
1960
+ const pendingDraftBoundaries = [];
1961
+ const latestQueuedDraftBoundaryOffsets = /* @__PURE__ */ new Map();
1962
+ let currentDraftReplyToId = draftReplyToId;
1963
+ let previewToolProgressSuppressed = false;
1964
+ let previewToolProgressLines = [];
1965
+ let latestPlan;
1966
+ let latestPlanExplanation;
1967
+ const progressConfigEntry = accountConfig ?? cfg.channels?.matrix;
1968
+ const progressSeed = `${accountId}:${roomId}`;
1969
+ const renderProgressDraft = () => {
1970
+ if (!draftStream) return;
1971
+ const previewText = formatChannelProgressDraftText({
1972
+ entry: progressConfigEntry,
1973
+ lines: previewToolProgressLines,
1974
+ seed: progressSeed,
1975
+ formatLine: formatMatrixToolProgressMarkdownCode,
1976
+ bullet: "-",
1977
+ narration: latestPlanExplanation,
1978
+ plan: latestPlan
1979
+ });
1980
+ if (!previewText) return;
1981
+ draftStream.update(previewText);
1982
+ };
1983
+ const progressDraftGate = createChannelProgressDraftGate({ onStart: renderProgressDraft });
1984
+ const pushPreviewToolProgress = async (line, options) => {
1985
+ if (!draftStream) return;
1986
+ if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
1987
+ const normalized = normalizeChannelProgressDraftLineIdentity(line);
1988
+ const progressLine = typeof line === "object" && line !== void 0 ? line : normalized;
1989
+ if (!progressDraftStreaming) {
1990
+ if (!shouldStreamPreviewToolProgress || previewToolProgressSuppressed || !normalized) return;
1991
+ const nextLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(progressConfigEntry) });
1992
+ if (nextLines === previewToolProgressLines) return;
1993
+ previewToolProgressLines = nextLines;
1994
+ draftStream.update(formatChannelProgressDraftText({
1995
+ entry: progressConfigEntry,
1996
+ lines: previewToolProgressLines,
1997
+ seed: progressSeed,
1998
+ formatLine: formatMatrixToolProgressMarkdownCode,
1999
+ bullet: "-",
2000
+ narration: latestPlanExplanation,
2001
+ plan: latestPlan
2002
+ }));
2003
+ return;
2004
+ }
2005
+ if (shouldStreamPreviewToolProgress && !previewToolProgressSuppressed && normalized) previewToolProgressLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(progressConfigEntry) });
2006
+ const alreadyStarted = progressDraftGate.hasStarted;
2007
+ const progressActive = await progressDraftGate.noteWork();
2008
+ if ((alreadyStarted || progressActive) && progressDraftGate.hasStarted) renderProgressDraft();
2009
+ };
2010
+ const pushPlanProgress = async (steps, explanation) => {
2011
+ latestPlan = steps?.length ? steps.map((entry) => ({ ...entry })) : void 0;
2012
+ latestPlanExplanation = explanation?.replace(/\s+/g, " ").trim() || void 0;
2013
+ if (!draftStream || previewToolProgressSuppressed) return;
2014
+ if (!progressDraftStreaming) {
2015
+ renderProgressDraft();
2016
+ return;
2017
+ }
2018
+ const alreadyStarted = progressDraftGate.hasStarted;
2019
+ await progressDraftGate.startNow();
2020
+ if (alreadyStarted && progressDraftGate.hasStarted) renderProgressDraft();
2021
+ };
2022
+ const suppressPreviewToolProgressForAnswerText = (text) => {
2023
+ if (!text?.trim()) return;
2024
+ previewToolProgressSuppressed = true;
2025
+ previewToolProgressLines = [];
2026
+ latestPlan = void 0;
2027
+ latestPlanExplanation = void 0;
2028
+ };
2029
+ const resetPreviewToolProgress = () => {
2030
+ previewToolProgressSuppressed = false;
2031
+ previewToolProgressLines = [];
2032
+ latestPlan = void 0;
2033
+ latestPlanExplanation = void 0;
2034
+ };
2035
+ const buildPreviewToolProgressReplyOptions = () => {
2036
+ if (!shouldSuppressDefaultToolProgressMessages) return {};
2037
+ const options = { suppressDefaultToolProgressMessages: true };
2038
+ if (!shouldStreamPreviewToolProgress) return options;
2039
+ return {
2040
+ ...options,
2041
+ onToolStart: async (payload) => {
2042
+ const toolName = payload.name?.trim();
2043
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
2044
+ event: "tool",
2045
+ itemId: payload.itemId,
2046
+ toolCallId: payload.toolCallId,
2047
+ name: toolName,
2048
+ phase: payload.phase,
2049
+ args: payload.args
2050
+ }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName });
2051
+ },
2052
+ onItemEvent: async (payload) => {
2053
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
2054
+ event: "item",
2055
+ itemId: payload.itemId,
2056
+ toolCallId: payload.toolCallId,
2057
+ itemKind: payload.kind,
2058
+ title: payload.title,
2059
+ name: payload.name,
2060
+ phase: payload.phase,
2061
+ status: payload.status,
2062
+ summary: payload.summary,
2063
+ progressText: payload.progressText,
2064
+ meta: payload.meta
2065
+ }));
2066
+ },
2067
+ onPlanUpdate: async (payload) => {
2068
+ if (payload.phase !== "update") return;
2069
+ await pushPlanProgress(payload.steps, payload.explanation);
2070
+ },
2071
+ onApprovalEvent: async (payload) => {
2072
+ if (payload.phase !== "requested") return;
2073
+ await pushPreviewToolProgress(formatChannelProgressDraftLine({
2074
+ event: "approval",
2075
+ phase: payload.phase,
2076
+ title: payload.title,
2077
+ command: payload.command,
2078
+ reason: payload.reason,
2079
+ message: payload.message
2080
+ }));
2081
+ },
2082
+ onCommandOutput: async (payload) => {
2083
+ if (payload.phase !== "end") return;
2084
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
2085
+ event: "command-output",
2086
+ itemId: payload.itemId,
2087
+ toolCallId: payload.toolCallId,
2088
+ phase: payload.phase,
2089
+ title: payload.title,
2090
+ name: payload.name,
2091
+ status: payload.status,
2092
+ exitCode: payload.exitCode
2093
+ }));
2094
+ },
2095
+ onPatchSummary: async (payload) => {
2096
+ if (payload.phase !== "end") return;
2097
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
2098
+ event: "patch",
2099
+ itemId: payload.itemId,
2100
+ toolCallId: payload.toolCallId,
2101
+ phase: payload.phase,
2102
+ title: payload.title,
2103
+ name: payload.name,
2104
+ added: payload.added,
2105
+ modified: payload.modified,
2106
+ deleted: payload.deleted,
2107
+ summary: payload.summary
2108
+ }));
2109
+ }
2110
+ };
2111
+ };
2112
+ const getDisplayableDraftText = () => {
2113
+ const nextDraftBoundaryOffset = pendingDraftBoundaries.find((boundary) => boundary.messageGeneration === currentDraftMessageGeneration)?.endOffset;
2114
+ if (nextDraftBoundaryOffset === void 0) return latestDraftFullText.slice(currentDraftBlockOffset);
2115
+ return latestDraftFullText.slice(currentDraftBlockOffset, nextDraftBoundaryOffset);
2116
+ };
2117
+ const updateDraftFromLatestFullText = () => {
2118
+ const blockText = getDisplayableDraftText();
2119
+ if (blockText) draftStream?.update(blockText);
2120
+ };
2121
+ const queueDraftBlockBoundary = (payload, context) => {
2122
+ const payloadTextLength = payload.text?.length ?? 0;
2123
+ const messageGeneration = context?.assistantMessageIndex ?? currentDraftMessageGeneration;
2124
+ const nextDraftBoundaryOffset = (latestQueuedDraftBoundaryOffsets.get(messageGeneration) ?? 0) + payloadTextLength;
2125
+ latestQueuedDraftBoundaryOffsets.set(messageGeneration, nextDraftBoundaryOffset);
2126
+ pendingDraftBoundaries.push({
2127
+ messageGeneration,
2128
+ endOffset: nextDraftBoundaryOffset
2129
+ });
2130
+ };
2131
+ const advanceDraftBlockBoundary = (options) => {
2132
+ const completedBoundary = pendingDraftBoundaries.shift();
2133
+ if (completedBoundary) {
2134
+ if (!pendingDraftBoundaries.some((entry) => entry.messageGeneration === completedBoundary.messageGeneration)) latestQueuedDraftBoundaryOffsets.delete(completedBoundary.messageGeneration);
2135
+ if (completedBoundary.messageGeneration === currentDraftMessageGeneration) currentDraftBlockOffset = completedBoundary.endOffset;
2136
+ return;
2137
+ }
2138
+ if (options?.fallbackToLatestEnd) currentDraftBlockOffset = latestDraftFullText.length;
2139
+ };
2140
+ const resetDraftBlockOffsets = () => {
2141
+ currentDraftMessageGeneration += 1;
2142
+ currentDraftBlockOffset = 0;
2143
+ latestDraftFullText = "";
2144
+ };
2145
+ const resetDraftDeliveryState = async () => {
2146
+ await draftStream?.discardPending();
2147
+ draftStream?.reset();
2148
+ draftConsumed = false;
2149
+ currentDraftMessageGeneration = 0;
2150
+ currentDraftBlockOffset = 0;
2151
+ latestDraftFullText = "";
2152
+ pendingDraftBoundaries.length = 0;
2153
+ latestQueuedDraftBoundaryOffsets.clear();
2154
+ currentDraftReplyToId = draftReplyToId;
2155
+ progressDraftGate.reset();
2156
+ resetPreviewToolProgress();
2157
+ };
2158
+ return {
2159
+ draftStream,
2160
+ progressDraftGate,
2161
+ buildPreviewToolProgressReplyOptions,
2162
+ queueDraftBlockBoundary,
2163
+ advanceDraftBlockBoundary,
2164
+ resetDraftBlockOffsets,
2165
+ resetPreviewToolProgress,
2166
+ resetDraftDeliveryState,
2167
+ updateDraftFromLatestFullText,
2168
+ isDraftConsumed: () => draftConsumed,
2169
+ markDraftConsumed: () => {
2170
+ draftConsumed = true;
2171
+ },
2172
+ clearDraftConsumed: () => {
2173
+ draftConsumed = false;
2174
+ },
2175
+ currentReplyToId: () => currentDraftReplyToId,
2176
+ setCurrentReplyToId: (replyToId) => {
2177
+ currentDraftReplyToId = replyToId;
2178
+ },
2179
+ resetReplyToIdForNextBlock: () => {
2180
+ currentDraftReplyToId = replyToMode === "all" ? draftReplyToId : void 0;
2181
+ },
2182
+ onPartialReply: (text) => {
2183
+ if (progressDraftStreaming) return;
2184
+ latestDraftFullText = text;
2185
+ suppressPreviewToolProgressForAnswerText(text);
2186
+ updateDraftFromLatestFullText();
2187
+ }
2188
+ };
2189
+ }
2190
+ //#endregion
2191
+ //#region extensions/matrix/src/matrix/monitor/room-history.ts
2192
+ /** Maximum entries retained per room (hard cap to bound memory). */
2193
+ const DEFAULT_MAX_QUEUE_SIZE = 200;
2194
+ /** Maximum number of rooms to retain queues for (FIFO eviction beyond this). */
2195
+ const DEFAULT_MAX_ROOM_QUEUES = 1e3;
2196
+ /** Maximum number of (agentId, roomId) watermark entries to retain. */
2197
+ const MAX_WATERMARK_ENTRIES = 5e3;
2198
+ /** Maximum prepared trigger snapshots retained per room for retry reuse. */
2199
+ const MAX_PREPARED_TRIGGER_ENTRIES = 500;
2200
+ /** Maximum thread queues retained per room (FIFO eviction beyond this). */
2201
+ const MAX_THREAD_QUEUES_PER_ROOM = 200;
2202
+ function createRoomHistoryTrackerInternal(maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES, maxWatermarkEntries = MAX_WATERMARK_ENTRIES, maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES) {
2203
+ const roomQueues = /* @__PURE__ */ new Map();
2204
+ /** Maps `{agentId, roomId, scope}` → absolute consumed-up-to index */
2205
+ const agentWatermarks = /* @__PURE__ */ new Map();
2206
+ let nextQueueGeneration = 1;
2207
+ function clearRoomWatermarks(roomId) {
2208
+ for (const key of agentWatermarks.keys()) if (JSON.parse(key)?.roomId === roomId) agentWatermarks.delete(key);
2209
+ }
2210
+ function clearThreadWatermarks(roomId, threadRootId) {
2211
+ for (const key of agentWatermarks.keys()) {
2212
+ const parsed = JSON.parse(key);
2213
+ if (parsed?.roomId === roomId && parsed.scope === threadRootId) agentWatermarks.delete(key);
2214
+ }
2215
+ }
2216
+ function createHistoryQueue() {
2217
+ return {
2218
+ entries: [],
2219
+ baseIndex: 0,
1933
2220
  generation: nextQueueGeneration++,
1934
2221
  preparedTriggers: /* @__PURE__ */ new Map()
1935
2222
  };
@@ -2141,200 +2428,1124 @@ function createRoomHistoryTracker(maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, maxRoom
2141
2428
  };
2142
2429
  }
2143
2430
  //#endregion
2144
- //#region extensions/matrix/src/matrix/monitor/thread-context.ts
2145
- const MAX_TRACKED_THREAD_STARTERS = 256;
2146
- const MAX_THREAD_STARTER_BODY_LENGTH = 500;
2147
- function truncateThreadStarterBody(value) {
2148
- if (value.length <= MAX_THREAD_STARTER_BODY_LENGTH) return value;
2149
- return `${sliceUtf16Safe(value, 0, MAX_THREAD_STARTER_BODY_LENGTH - 3)}...`;
2431
+ //#region extensions/matrix/src/matrix/monitor/handler-ingress-access.ts
2432
+ async function resolveMatrixIngressAccess(config) {
2433
+ const { handler, params: paramsLocal, roomId, event, eventTs, senderId, isReactionEvent, readStoreAllowFrom, shouldSendPairingReply, resolveLiveAccountAllowlists, roomHistoryTracker, commitInboundEventIfClaimed } = config;
2434
+ const { core, cfg, accountId, accountConfig, client, logVerboseMessage, groupPolicy, dmEnabled, dmPolicy, dmThreadReplies, threadReplies, roomsConfig, accountAllowBots, configuredBotUserIds, needsRoomAliasesForConfig, getRoomInfo, getMemberDisplayName } = handler;
2435
+ const content = paramsLocal.content;
2436
+ const isDirectMessage = paramsLocal.isDirectMessage;
2437
+ const isRoom = !isDirectMessage;
2438
+ const { audioPreflightMode, locationPayload, reservedHistorySlot, selfUserId } = paramsLocal;
2439
+ const messageId = event.event_id ?? "";
2440
+ const threadRootId = resolveMatrixThreadRootId({
2441
+ event,
2442
+ content
2443
+ });
2444
+ const thread = resolveMatrixThreadRouting({
2445
+ isDirectMessage,
2446
+ threadReplies,
2447
+ dmThreadReplies,
2448
+ messageId,
2449
+ threadRootId
2450
+ });
2451
+ const historyThreadId = threadRootId ? thread.threadId : void 0;
2452
+ let reservedHistorySlotConsumed = false;
2453
+ const discardReservedHistorySlot = () => {
2454
+ if (reservedHistorySlot && !reservedHistorySlotConsumed) {
2455
+ roomHistoryTracker.discardPending(roomId, reservedHistorySlot, historyThreadId);
2456
+ reservedHistorySlotConsumed = true;
2457
+ }
2458
+ };
2459
+ const markReservedHistorySlotConsumed = () => {
2460
+ reservedHistorySlotConsumed = true;
2461
+ };
2462
+ const commitInboundEventIfClaimedAndDiscardReserved = async () => {
2463
+ discardReservedHistorySlot();
2464
+ await commitInboundEventIfClaimed();
2465
+ };
2466
+ if (isRoom && groupPolicy === "disabled") {
2467
+ await commitInboundEventIfClaimedAndDiscardReserved();
2468
+ return;
2469
+ }
2470
+ const roomInfoForConfig = isRoom && needsRoomAliasesForConfig ? await getRoomInfo(roomId, { includeAliases: true }) : void 0;
2471
+ const roomAliasesForConfig = roomInfoForConfig ? [roomInfoForConfig.canonicalAlias ?? "", ...roomInfoForConfig.altAliases].filter(Boolean) : [];
2472
+ const roomConfigInfo = isRoom ? resolveMatrixRoomConfig({
2473
+ rooms: roomsConfig,
2474
+ roomId,
2475
+ aliases: roomAliasesForConfig
2476
+ }) : void 0;
2477
+ const roomConfig = roomConfigInfo?.config;
2478
+ const allowBotsMode = resolveMatrixAllowBotsMode(roomConfig?.allowBots ?? accountAllowBots);
2479
+ const isConfiguredBotSender = configuredBotUserIds.has(senderId);
2480
+ const roomMatchMeta = roomConfigInfo ? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${roomConfigInfo.matchSource ?? "none"}` : "matchKey=none matchSource=none";
2481
+ if (isConfiguredBotSender && allowBotsMode === "off") {
2482
+ logVerboseMessage(`matrix: drop configured bot sender=${senderId} (allowBots=false${isDirectMessage ? "" : `, ${roomMatchMeta}`})`);
2483
+ await commitInboundEventIfClaimedAndDiscardReserved();
2484
+ return;
2485
+ }
2486
+ const botLoopProtection = isConfiguredBotSender && senderId !== selfUserId ? {
2487
+ scopeId: accountId,
2488
+ conversationId: roomId,
2489
+ senderId,
2490
+ receiverId: selfUserId,
2491
+ config: mergePairLoopGuardConfig(accountConfig?.botLoopProtection, roomConfig?.botLoopProtection),
2492
+ defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
2493
+ defaultEnabled: true,
2494
+ nowMs: eventTs ?? void 0
2495
+ } : void 0;
2496
+ if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
2497
+ logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
2498
+ await commitInboundEventIfClaimedAndDiscardReserved();
2499
+ return;
2500
+ }
2501
+ if (isRoom && groupPolicy === "allowlist") {
2502
+ if (!roomConfigInfo?.allowlistConfigured) {
2503
+ logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
2504
+ await commitInboundEventIfClaimedAndDiscardReserved();
2505
+ return;
2506
+ }
2507
+ if (!roomConfig) {
2508
+ logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
2509
+ await commitInboundEventIfClaimedAndDiscardReserved();
2510
+ return;
2511
+ }
2512
+ }
2513
+ let senderNamePromise = null;
2514
+ const getSenderName = async () => {
2515
+ senderNamePromise ??= getMemberDisplayName(roomId, senderId).catch(() => senderId);
2516
+ return await senderNamePromise;
2517
+ };
2518
+ const storeAllowFrom = isDirectMessage && dmPolicy !== "allowlist" && dmPolicy !== "open" ? await readStoreAllowFrom() : [];
2519
+ const roomUsers = roomConfig?.users ?? [];
2520
+ const { liveDmAllowFrom, liveGroupAllowFrom } = await resolveLiveAccountAllowlists();
2521
+ const accessState = await resolveMatrixMonitorAccessState({
2522
+ allowFrom: liveDmAllowFrom,
2523
+ storeAllowFrom,
2524
+ dmPolicy,
2525
+ groupPolicy,
2526
+ groupAllowFrom: liveGroupAllowFrom,
2527
+ roomUsers,
2528
+ senderId,
2529
+ isRoom,
2530
+ accountId,
2531
+ eventKind: isReactionEvent ? "reaction" : "message"
2532
+ });
2533
+ const { effectiveGroupAllowFrom, effectiveRoomUsers, messageIngress } = accessState;
2534
+ const ingressDecision = messageIngress.ingress;
2535
+ if (isDirectMessage) {
2536
+ if (!dmEnabled || dmPolicy === "disabled") {
2537
+ await commitInboundEventIfClaimedAndDiscardReserved();
2538
+ return;
2539
+ }
2540
+ const senderReason = messageIngress.senderAccess.reasonCode;
2541
+ if (ingressDecision.decision !== "allow") {
2542
+ if (ingressDecision.admission === "pairing-required") {
2543
+ const senderName = await getSenderName();
2544
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
2545
+ channel: "matrix",
2546
+ id: senderId,
2547
+ accountId,
2548
+ meta: { name: senderName }
2549
+ });
2550
+ if (shouldSendPairingReply(senderId, created)) {
2551
+ const pairingReply = core.channel.pairing.buildPairingReply({
2552
+ channel: "matrix",
2553
+ idLine: `Your Matrix user id: ${senderId}`,
2554
+ code
2555
+ });
2556
+ logVerboseMessage(created ? `matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (reason=${senderReason})` : `matrix pairing reminder sender=${senderId} name=${senderName ?? "unknown"} (reason=${senderReason})`);
2557
+ try {
2558
+ const { sendMessageMatrix } = await loadMatrixSendModule();
2559
+ await sendMessageMatrix(`room:${roomId}`, created ? pairingReply : `${pairingReply}\n\nPairing request is still pending approval. Reusing existing code.`, {
2560
+ client,
2561
+ cfg,
2562
+ accountId
2563
+ });
2564
+ await commitInboundEventIfClaimed();
2565
+ } catch (err) {
2566
+ logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
2567
+ discardReservedHistorySlot();
2568
+ return;
2569
+ }
2570
+ } else {
2571
+ logVerboseMessage(`matrix pairing reminder suppressed sender=${senderId} (cooldown)`);
2572
+ await commitInboundEventIfClaimedAndDiscardReserved();
2573
+ }
2574
+ }
2575
+ if (isReactionEvent || dmPolicy !== "pairing") {
2576
+ logVerboseMessage(`matrix: blocked ${isReactionEvent ? "reaction" : "dm"} sender ${senderId} (dmPolicy=${dmPolicy}, reason=${senderReason})`);
2577
+ await commitInboundEventIfClaimedAndDiscardReserved();
2578
+ }
2579
+ return;
2580
+ }
2581
+ }
2582
+ if (isRoom && ingressDecision.decision !== "allow") {
2583
+ logVerboseMessage(`matrix: blocked sender ${senderId} (ingress=${ingressDecision.reasonCode}, ${roomMatchMeta})`);
2584
+ await commitInboundEventIfClaimedAndDiscardReserved();
2585
+ return;
2586
+ }
2587
+ if (isRoom) logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`);
2588
+ if (isReactionEvent) {
2589
+ const senderName = await getSenderName();
2590
+ const { handleInboundMatrixReaction } = await loadMatrixReactionEvents();
2591
+ await handleInboundMatrixReaction({
2592
+ client,
2593
+ core,
2594
+ cfg,
2595
+ accountId,
2596
+ roomId,
2597
+ event,
2598
+ senderId,
2599
+ senderLabel: senderName,
2600
+ selfUserId,
2601
+ isDirectMessage,
2602
+ logVerboseMessage
2603
+ });
2604
+ await commitInboundEventIfClaimedAndDiscardReserved();
2605
+ return;
2606
+ }
2607
+ return {
2608
+ content,
2609
+ messageId,
2610
+ audioPreflightMode,
2611
+ isDirectMessage,
2612
+ isRoom,
2613
+ locationPayload,
2614
+ reservedHistorySlot,
2615
+ threadRootId,
2616
+ thread,
2617
+ historyThreadId,
2618
+ discardReservedHistorySlot,
2619
+ markReservedHistorySlotConsumed,
2620
+ commitInboundEventIfClaimedAndDiscardReserved,
2621
+ roomConfig,
2622
+ allowBotsMode,
2623
+ isConfiguredBotSender,
2624
+ selfUserId,
2625
+ botLoopProtection,
2626
+ roomMatchMeta,
2627
+ getSenderName,
2628
+ accessState,
2629
+ effectiveGroupAllowFrom,
2630
+ effectiveRoomUsers
2631
+ };
2150
2632
  }
2151
- function summarizeMatrixThreadStarterEvent(event) {
2152
- const body = summarizeMatrixMessageContextEvent(event);
2153
- if (body) return truncateThreadStarterBody(body);
2154
- const content = event.content;
2155
- const msgtype = trimMatrixMaybeString(content.msgtype);
2156
- if (msgtype) return `Matrix ${msgtype} message`;
2157
- const eventType = trimMatrixMaybeString(event.type);
2158
- return eventType ? `Matrix ${eventType} event` : void 0;
2633
+ //#endregion
2634
+ //#region extensions/matrix/src/matrix/monitor/media.ts
2635
+ const MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS = 3e4;
2636
+ async function fetchMatrixMediaBuffer(params) {
2637
+ try {
2638
+ return { buffer: await params.client.downloadContent(params.mxcUrl, {
2639
+ maxBytes: params.maxBytes,
2640
+ readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS
2641
+ }) };
2642
+ } catch (err) {
2643
+ if (isMatrixMediaSizeLimitError(err)) throw err;
2644
+ throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err });
2645
+ }
2159
2646
  }
2160
- function formatMatrixThreadStarterBody(params) {
2161
- const senderLabel = params.senderName ?? params.senderId ?? "unknown sender";
2162
- const lines = [`Matrix thread root ${params.threadRootId} from ${senderLabel}:`];
2163
- if (params.summary) lines.push(params.summary);
2164
- return lines.join("\n");
2647
+ /**
2648
+ * Download and decrypt encrypted media from a Matrix room.
2649
+ * Uses the Matrix crypto adapter's decryptMedia helper.
2650
+ */
2651
+ async function fetchEncryptedMediaBuffer(params) {
2652
+ if (!params.client.crypto) throw new Error("Cannot decrypt media: crypto not enabled");
2653
+ const decrypted = await params.client.crypto.decryptMedia(params.file, {
2654
+ maxBytes: params.maxBytes,
2655
+ readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS
2656
+ });
2657
+ if (decrypted.byteLength > params.maxBytes) throw new MatrixMediaSizeLimitError();
2658
+ return { buffer: decrypted };
2165
2659
  }
2166
- function createMatrixThreadContextResolver(params) {
2167
- const cache = /* @__PURE__ */ new Map();
2168
- const remember = (key, value) => {
2169
- cache.set(key, value);
2170
- if (cache.size > MAX_TRACKED_THREAD_STARTERS) {
2171
- const oldest = cache.keys().next().value;
2172
- if (typeof oldest === "string") cache.delete(oldest);
2173
- }
2174
- return value;
2660
+ async function downloadMatrixMedia(params) {
2661
+ let fetched;
2662
+ if (typeof params.sizeBytes === "number" && params.sizeBytes > params.maxBytes) throw new MatrixMediaSizeLimitError();
2663
+ if (params.file) fetched = await fetchEncryptedMediaBuffer({
2664
+ client: params.client,
2665
+ file: params.file,
2666
+ maxBytes: params.maxBytes
2667
+ });
2668
+ else fetched = await fetchMatrixMediaBuffer({
2669
+ client: params.client,
2670
+ mxcUrl: params.mxcUrl,
2671
+ maxBytes: params.maxBytes
2672
+ });
2673
+ if (!fetched) return null;
2674
+ const headerType = params.contentType ?? void 0;
2675
+ const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(fetched.buffer, headerType, "inbound", params.maxBytes, params.originalFilename);
2676
+ return {
2677
+ path: saved.path,
2678
+ contentType: saved.contentType,
2679
+ placeholder: "[matrix media]"
2680
+ };
2681
+ }
2682
+ //#endregion
2683
+ //#region extensions/matrix/src/matrix/monitor/mentions.ts
2684
+ const MATRIX_HTML_ENTITY_RE = /&(?:#x?[0-9a-f]+|amp|apos|gt|lt|nbsp|quot);/gi;
2685
+ function decodeVisibleHtmlEntities(value) {
2686
+ return value.replace(MATRIX_HTML_ENTITY_RE, (entity) => {
2687
+ const decoded = decodeHtmlEntities(entity.startsWith("&#") ? entity : entity.toLowerCase());
2688
+ return decoded === "\xA0" ? " " : decoded;
2689
+ });
2690
+ }
2691
+ function normalizeVisibleMentionText(value) {
2692
+ return normalizeLowercaseStringOrEmpty(decodeVisibleHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")).replace(/\s+/g, " "));
2693
+ }
2694
+ function extractVisibleMentionText(value) {
2695
+ return normalizeVisibleMentionText(value ?? "");
2696
+ }
2697
+ function resolveMatrixUserLocalpart(userId) {
2698
+ const trimmed = userId.trim();
2699
+ if (!trimmed.startsWith("@")) return null;
2700
+ const colonIndex = trimmed.indexOf(":");
2701
+ if (colonIndex <= 1) return null;
2702
+ return trimmed.slice(1, colonIndex).trim() || null;
2703
+ }
2704
+ function resolveMatrixMentionPrefixCandidates(params) {
2705
+ const candidates = [];
2706
+ const seen = /* @__PURE__ */ new Set();
2707
+ const append = (candidate) => {
2708
+ const trimmed = candidate?.trim();
2709
+ if (!trimmed) return;
2710
+ const normalized = normalizeLowercaseStringOrEmpty(trimmed);
2711
+ if (seen.has(normalized)) return;
2712
+ seen.add(normalized);
2713
+ candidates.push(trimmed);
2714
+ };
2715
+ append(params.userId);
2716
+ const localpart = params.userId ? resolveMatrixUserLocalpart(params.userId) : null;
2717
+ append(localpart ? `@${localpart}` : null);
2718
+ append(params.displayName);
2719
+ append(params.displayName ? `@${params.displayName}` : null);
2720
+ append(params.displayName ? `@[${params.displayName}]` : null);
2721
+ return candidates;
2722
+ }
2723
+ function stripMatchedMatrixMentionPrefix(text, pattern) {
2724
+ const match = text.match(pattern);
2725
+ if (!match) return null;
2726
+ return text.slice(match[0].length).trimStart();
2727
+ }
2728
+ function stripNativeMatrixMentionPrefix(text, candidate) {
2729
+ return stripMatchedMatrixMentionPrefix(text, new RegExp(`^\\s*${escapeRegExp(candidate)}(?:\\s*[:,])?(?:\\s+|$)`, "i"));
2730
+ }
2731
+ function stripRegexMatrixMentionPrefix(text, pattern) {
2732
+ const flags = pattern.flags.replace(/[gy]/g, "");
2733
+ return stripMatchedMatrixMentionPrefix(text, new RegExp(`^\\s*(?:${pattern.source})(?:\\s*[:,])?(?:\\s+|$)`, flags));
2734
+ }
2735
+ function stripMatrixMentionPrefix(params) {
2736
+ const text = params.text;
2737
+ if (!text) return text;
2738
+ for (const candidate of resolveMatrixMentionPrefixCandidates(params)) {
2739
+ const stripped = stripNativeMatrixMentionPrefix(text, candidate);
2740
+ if (stripped !== null) return stripped;
2741
+ }
2742
+ for (const pattern of params.mentionRegexes ?? []) {
2743
+ const stripped = stripRegexMatrixMentionPrefix(text, pattern);
2744
+ if (stripped !== null) return stripped;
2745
+ }
2746
+ return text;
2747
+ }
2748
+ function isVisibleMentionLabel(params) {
2749
+ const cleaned = extractVisibleMentionText(params.text);
2750
+ if (!cleaned) return false;
2751
+ if (params.mentionRegexes.some((pattern) => pattern.test(cleaned))) return true;
2752
+ const localpart = resolveMatrixUserLocalpart(params.userId);
2753
+ return [
2754
+ extractVisibleMentionText(params.userId),
2755
+ localpart ? extractVisibleMentionText(localpart) : null,
2756
+ localpart ? extractVisibleMentionText(`@${localpart}`) : null,
2757
+ params.displayName ? extractVisibleMentionText(params.displayName) : null,
2758
+ params.displayName ? extractVisibleMentionText(`@${params.displayName}`) : null,
2759
+ params.displayName ? extractVisibleMentionText(`@[${params.displayName}]`) : null
2760
+ ].filter((value) => Boolean(value)).includes(cleaned);
2761
+ }
2762
+ function hasVisibleRoomMention(value) {
2763
+ const cleaned = extractVisibleMentionText(value);
2764
+ return /(^|[^a-z0-9_])@room\b/i.test(cleaned);
2765
+ }
2766
+ /**
2767
+ * Check if formatted_body contains a matrix.to link whose visible label still
2768
+ * looks like a real mention for the given user. Do not trust href alone, since
2769
+ * senders can hide arbitrary matrix.to links behind unrelated link text.
2770
+ * Many Matrix clients (including Element) use HTML links in formatted_body instead of
2771
+ * or in addition to the m.mentions field.
2772
+ */
2773
+ function checkFormattedBodyMention(params) {
2774
+ if (!params.formattedBody || !params.userId) return false;
2775
+ for (const match of params.formattedBody.matchAll(/<a\b[^>]*href=(["'])(https:\/\/matrix\.to\/#[^"']+)\1[^>]*>(.*?)<\/a>/gis)) {
2776
+ const href = match[2];
2777
+ const visibleLabel = match[3] ?? "";
2778
+ if (!href) continue;
2779
+ try {
2780
+ const parsed = new URL(href);
2781
+ if (decodeURIComponent(parsed.hash.replace(/^#\/?/, "").trim()) !== params.userId.trim()) continue;
2782
+ if (isVisibleMentionLabel({
2783
+ text: visibleLabel,
2784
+ userId: params.userId,
2785
+ mentionRegexes: params.mentionRegexes,
2786
+ displayName: params.displayName
2787
+ })) return true;
2788
+ } catch {
2789
+ continue;
2790
+ }
2791
+ }
2792
+ return false;
2793
+ }
2794
+ function resolveMentions(params) {
2795
+ const mentions = params.content["m.mentions"];
2796
+ const mentionedUsers = Array.isArray(mentions?.user_ids) ? new Set(mentions.user_ids) : /* @__PURE__ */ new Set();
2797
+ const textMentioned = getMatrixRuntime().channel.mentions.matchesMentionPatterns(params.text ?? "", params.mentionRegexes);
2798
+ const visibleRoomMention = hasVisibleRoomMention(params.text) || hasVisibleRoomMention(params.content.formatted_body);
2799
+ const mentionedInFormattedBody = params.userId ? checkFormattedBodyMention({
2800
+ formattedBody: params.content.formatted_body,
2801
+ userId: params.userId,
2802
+ displayName: params.displayName,
2803
+ mentionRegexes: params.mentionRegexes
2804
+ }) : false;
2805
+ const metadataBackedUserMention = Boolean(params.userId && mentionedUsers.has(params.userId) && (mentionedInFormattedBody || textMentioned));
2806
+ const metadataBackedRoomMention = Boolean(mentions?.room) && visibleRoomMention;
2807
+ const explicitMention = mentionedInFormattedBody || metadataBackedUserMention || metadataBackedRoomMention;
2808
+ return {
2809
+ wasMentioned: explicitMention || textMentioned || visibleRoomMention,
2810
+ hasExplicitMention: explicitMention
2811
+ };
2812
+ }
2813
+ //#endregion
2814
+ //#region extensions/matrix/src/matrix/monitor/handler-ingress-content.ts
2815
+ async function resolveMatrixIngressContent(config) {
2816
+ const { handler, params: paramsLocal, access, roomId, event, eventType, isPollEvent, eventTs, senderId, roomHistoryTracker, commitInboundEventIfClaimed } = config;
2817
+ const { client, core, cfg, accountId, accountConfig, logger, logVerboseMessage, historyLimit, mediaMaxBytes, dmSessionScope, getMemberDisplayName } = handler;
2818
+ const { content: accessContent, messageId, audioPreflightMode, isDirectMessage, isRoom, locationPayload, reservedHistorySlot, threadRootId, thread, historyThreadId, discardReservedHistorySlot, markReservedHistorySlotConsumed, commitInboundEventIfClaimedAndDiscardReserved, roomConfig, allowBotsMode, isConfiguredBotSender, selfUserId, botLoopProtection, roomMatchMeta, getSenderName, accessState, effectiveGroupAllowFrom, effectiveRoomUsers } = access;
2819
+ let content = accessContent;
2820
+ let pollSnapshotPromise = null;
2821
+ const getPollSnapshot = async () => {
2822
+ if (!isPollEvent) return null;
2823
+ pollSnapshotPromise ??= fetchMatrixPollSnapshot(client, roomId, event).catch((err) => {
2824
+ logVerboseMessage(`matrix: failed resolving poll snapshot room=${roomId} id=${event.event_id ?? "unknown"}: ${String(err)}`);
2825
+ return null;
2826
+ });
2827
+ return await pollSnapshotPromise;
2828
+ };
2829
+ const mentionPrecheckText = resolveMatrixMentionPrecheckText({
2830
+ eventType,
2831
+ content,
2832
+ locationText: locationPayload?.text
2833
+ });
2834
+ const contentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
2835
+ const contentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
2836
+ const mediaUrl = contentUrl ?? contentFile?.url;
2837
+ const earlyContentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
2838
+ const earlyContentType = earlyContentInfo?.mimetype;
2839
+ const earlyContentSize = typeof earlyContentInfo?.size === "number" ? earlyContentInfo.size : void 0;
2840
+ const earlyContentBody = typeof content.body === "string" ? content.body.trim() : "";
2841
+ const earlyOriginalFilename = (typeof content.filename === "string" ? content.filename.trim() : "") || earlyContentBody || void 0;
2842
+ const pendingHistoryText = resolveMatrixPendingHistoryText({
2843
+ mentionPrecheckText,
2844
+ content,
2845
+ mediaUrl
2846
+ });
2847
+ const pendingHistoryPollText = !pendingHistoryText && isPollEvent && historyLimit > 0 ? (await getPollSnapshot())?.text : "";
2848
+ if (!mentionPrecheckText && !mediaUrl && !isPollEvent) {
2849
+ await commitInboundEventIfClaimedAndDiscardReserved();
2850
+ return;
2851
+ }
2852
+ let preflightMedia = null;
2853
+ let preflightMediaDownloadFailed = false;
2854
+ let preflightMediaSizeLimitExceeded = false;
2855
+ let preflightAudioTranscript;
2856
+ const { route: _route, configuredBinding: _configuredBinding, runtimeBindingId: _runtimeBindingId } = resolveMatrixInboundRoute({
2857
+ cfg,
2858
+ accountId,
2859
+ roomId,
2860
+ senderId,
2861
+ isDirectMessage,
2862
+ dmSessionScope,
2863
+ threadId: thread.threadId,
2864
+ eventTs: eventTs ?? void 0,
2865
+ resolveAgentRoute: core.channel.routing.resolveAgentRoute
2866
+ });
2867
+ const hasExplicitSessionBinding = _configuredBinding !== null || _runtimeBindingId !== null;
2868
+ const preflightAudioMediaUrl = mediaUrl?.startsWith("mxc://") ? mediaUrl : void 0;
2869
+ const shouldRunMatrixAudioPreflight = isMatrixAudioContent({
2870
+ msgtype: typeof content.msgtype === "string" ? content.msgtype : void 0,
2871
+ mimetype: earlyContentType
2872
+ }) && isMatrixAudioMediaEnabled(cfg) && preflightAudioMediaUrl !== void 0;
2873
+ if (shouldRunMatrixAudioPreflight && audioPreflightMode === "defer" && isRoom && historyLimit > 0 && !reservedHistorySlot) {
2874
+ const reserved = roomHistoryTracker.reservePending(_route.agentId, roomId, {
2875
+ sender: senderId,
2876
+ body: pendingHistoryText,
2877
+ timestamp: eventTs ?? void 0,
2878
+ messageId
2879
+ }, historyThreadId);
2880
+ return { deferredPrefix: {
2881
+ ...paramsLocal,
2882
+ audioPreflightMode: "run",
2883
+ reservedHistorySlot: reserved
2884
+ } };
2885
+ }
2886
+ if (shouldRunMatrixAudioPreflight) {
2887
+ try {
2888
+ preflightMedia = await downloadMatrixMedia({
2889
+ client,
2890
+ mxcUrl: preflightAudioMediaUrl,
2891
+ contentType: earlyContentType,
2892
+ sizeBytes: earlyContentSize,
2893
+ maxBytes: mediaMaxBytes,
2894
+ file: contentFile,
2895
+ originalFilename: earlyOriginalFilename
2896
+ });
2897
+ } catch (err) {
2898
+ preflightMediaDownloadFailed = true;
2899
+ if (isMatrixMediaSizeLimitError(err)) preflightMediaSizeLimitExceeded = true;
2900
+ const errorText = formatMatrixErrorMessage(err);
2901
+ logVerboseMessage(`matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`);
2902
+ logger.warn("matrix media download failed", {
2903
+ roomId,
2904
+ eventId: event.event_id,
2905
+ msgtype: content.msgtype,
2906
+ encrypted: Boolean(contentFile),
2907
+ error: errorText
2908
+ });
2909
+ }
2910
+ if (preflightMedia) preflightAudioTranscript = await resolveMatrixPreflightAudioTranscript({
2911
+ mediaPath: preflightMedia.path,
2912
+ mediaContentType: preflightMedia.contentType,
2913
+ cfg,
2914
+ accountId,
2915
+ chatType: isDirectMessage ? "direct" : "channel",
2916
+ originatingTo: `room:${roomId}`,
2917
+ messageThreadId: thread.threadId,
2918
+ sessionKey: _route.sessionKey
2919
+ });
2920
+ }
2921
+ const agentMentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, _route.agentId, {
2922
+ provider: "matrix",
2923
+ conversationId: roomId,
2924
+ providerPolicy: accountConfig?.mentionPatterns
2925
+ });
2926
+ const selfDisplayName = content.formatted_body ? await getMemberDisplayName(roomId, selfUserId).catch(() => void 0) : void 0;
2927
+ const mentionPrecheckTextWithTranscript = preflightAudioTranscript ? [mentionPrecheckText, preflightAudioTranscript].filter(Boolean).join("\n").trim() : mentionPrecheckText;
2928
+ const { wasMentioned, hasExplicitMention } = resolveMentions({
2929
+ content,
2930
+ userId: selfUserId,
2931
+ displayName: selfDisplayName,
2932
+ text: mentionPrecheckTextWithTranscript,
2933
+ mentionRegexes: agentMentionRegexes
2934
+ });
2935
+ if (isConfiguredBotSender && allowBotsMode === "mentions" && !isDirectMessage && !wasMentioned) {
2936
+ logVerboseMessage(`matrix: drop configured bot sender=${senderId} (allowBots=mentions, missing mention, ${roomMatchMeta})`);
2937
+ await commitInboundEventIfClaimedAndDiscardReserved();
2938
+ return;
2939
+ }
2940
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
2941
+ cfg,
2942
+ surface: "matrix"
2943
+ });
2944
+ const useAccessGroups = true;
2945
+ const commandCheckText = stripMatrixMentionPrefix({
2946
+ text: mentionPrecheckText,
2947
+ userId: selfUserId,
2948
+ displayName: selfDisplayName,
2949
+ mentionRegexes: agentMentionRegexes
2950
+ });
2951
+ const hasControlCommandInMessage = core.channel.text.hasControlCommand(commandCheckText, cfg);
2952
+ const commandAccess = await resolveMatrixMonitorCommandAccess(accessState, {
2953
+ useAccessGroups,
2954
+ allowTextCommands,
2955
+ hasControlCommand: hasControlCommandInMessage
2956
+ });
2957
+ const commandAuthorized = commandAccess.authorized;
2958
+ if (isRoom && commandAccess.shouldBlockControlCommand) {
2959
+ logInboundDrop({
2960
+ log: logVerboseMessage,
2961
+ channel: "matrix",
2962
+ reason: "control command (unauthorized)",
2963
+ target: senderId
2964
+ });
2965
+ await commitInboundEventIfClaimedAndDiscardReserved();
2966
+ return;
2967
+ }
2968
+ const shouldRequireMention = isRoom ? roomConfig?.autoReply === true ? false : roomConfig?.autoReply === false ? true : typeof roomConfig?.requireMention === "boolean" ? roomConfig?.requireMention : true : false;
2969
+ const mentionDecision = resolveInboundMentionDecision({
2970
+ facts: {
2971
+ canDetectMention: true,
2972
+ wasMentioned,
2973
+ hasAnyMention: hasExplicitMention
2974
+ },
2975
+ policy: {
2976
+ isGroup: isRoom,
2977
+ requireMention: shouldRequireMention,
2978
+ allowTextCommands,
2979
+ hasControlCommand: hasControlCommandInMessage,
2980
+ commandAuthorized
2981
+ }
2982
+ });
2983
+ const { effectiveWasMentioned, shouldBypassMention } = mentionDecision;
2984
+ const canDetectMention = agentMentionRegexes.length > 0 || hasExplicitMention;
2985
+ if (mentionDecision.shouldSkip) {
2986
+ const pendingHistoryBody = preflightAudioTranscript ? formatMatrixAudioTranscript(preflightAudioTranscript) : pendingHistoryText || pendingHistoryPollText;
2987
+ if (historyLimit > 0 && pendingHistoryBody) {
2988
+ const pendingEntry = {
2989
+ sender: senderId,
2990
+ body: pendingHistoryBody,
2991
+ timestamp: eventTs ?? void 0,
2992
+ messageId
2993
+ };
2994
+ if (reservedHistorySlot) {
2995
+ roomHistoryTracker.finalizePending(roomId, reservedHistorySlot, pendingEntry, historyThreadId);
2996
+ markReservedHistorySlotConsumed();
2997
+ } else roomHistoryTracker.recordPending(roomId, pendingEntry, historyThreadId);
2998
+ }
2999
+ logger.info("skipping room message", {
3000
+ roomId,
3001
+ reason: "no-mention"
3002
+ });
3003
+ await commitInboundEventIfClaimed();
3004
+ return;
3005
+ }
3006
+ if (preflightAudioTranscript) await sendMatrixPreflightAudioTranscriptEcho({
3007
+ transcript: preflightAudioTranscript,
3008
+ cfg,
3009
+ accountId,
3010
+ originatingTo: `room:${roomId}`,
3011
+ messageThreadId: thread.threadId
3012
+ });
3013
+ if (isPollEvent) {
3014
+ const pollSnapshot = await getPollSnapshot();
3015
+ if (!pollSnapshot) {
3016
+ discardReservedHistorySlot();
3017
+ return;
3018
+ }
3019
+ content = {
3020
+ msgtype: "m.text",
3021
+ body: pollSnapshot.text
3022
+ };
3023
+ }
3024
+ let media = preflightMedia;
3025
+ let mediaDownloadFailed = preflightMediaDownloadFailed;
3026
+ let mediaSizeLimitExceeded = preflightMediaSizeLimitExceeded;
3027
+ const finalContentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
3028
+ const finalContentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
3029
+ const finalMediaUrl = finalContentUrl ?? finalContentFile?.url;
3030
+ const contentBody = typeof content.body === "string" ? content.body.trim() : "";
3031
+ const originalFilename = (typeof content.filename === "string" ? content.filename.trim() : "") || contentBody || void 0;
3032
+ const contentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
3033
+ const contentType = contentInfo?.mimetype;
3034
+ const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : void 0;
3035
+ if (!media && !mediaDownloadFailed && finalMediaUrl?.startsWith("mxc://")) try {
3036
+ media = await downloadMatrixMedia({
3037
+ client,
3038
+ mxcUrl: finalMediaUrl,
3039
+ contentType,
3040
+ sizeBytes: contentSize,
3041
+ maxBytes: mediaMaxBytes,
3042
+ file: finalContentFile,
3043
+ originalFilename
3044
+ });
3045
+ } catch (err) {
3046
+ mediaDownloadFailed = true;
3047
+ if (isMatrixMediaSizeLimitError(err)) mediaSizeLimitExceeded = true;
3048
+ const errorText = formatMatrixErrorMessage(err);
3049
+ logVerboseMessage(`matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`);
3050
+ logger.warn("matrix media download failed", {
3051
+ roomId,
3052
+ eventId: event.event_id,
3053
+ msgtype: content.msgtype,
3054
+ encrypted: Boolean(finalContentFile),
3055
+ error: errorText
3056
+ });
3057
+ }
3058
+ let bodyText = resolveMatrixInboundBodyText({
3059
+ rawBody: locationPayload?.text ?? contentBody,
3060
+ filename: typeof content.filename === "string" ? content.filename : void 0,
3061
+ mediaPlaceholder: media?.placeholder,
3062
+ msgtype: content.msgtype,
3063
+ hadMediaUrl: Boolean(finalMediaUrl),
3064
+ mediaDownloadFailed,
3065
+ mediaSizeLimitExceeded
3066
+ });
3067
+ if (preflightMedia && bodyText && bodyText !== preflightMedia.placeholder && isLikelyBareFilename(bodyText)) bodyText = preflightMedia.placeholder;
3068
+ if (preflightAudioTranscript) {
3069
+ const transcriptBody = formatMatrixAudioTranscript(preflightAudioTranscript);
3070
+ bodyText = !bodyText || bodyText === media?.placeholder ? transcriptBody : `${bodyText}\n${transcriptBody}`;
3071
+ }
3072
+ if (!bodyText) {
3073
+ await commitInboundEventIfClaimedAndDiscardReserved();
3074
+ return;
3075
+ }
3076
+ const commandBodyText = hasControlCommandInMessage ? commandCheckText : bodyText;
3077
+ const senderName = await getSenderName();
3078
+ if (_configuredBinding) {
3079
+ const { ensureConfiguredAcpBindingReady } = await loadAcpBindingRuntime();
3080
+ if (!(await ensureConfiguredAcpBindingReady({
3081
+ cfg,
3082
+ configuredBinding: _configuredBinding
3083
+ })).ok) {
3084
+ logInboundDrop({
3085
+ log: logVerboseMessage,
3086
+ channel: "matrix",
3087
+ reason: "configured ACP binding unavailable",
3088
+ target: _configuredBinding.spec.conversationId
3089
+ });
3090
+ discardReservedHistorySlot();
3091
+ return;
3092
+ }
3093
+ }
3094
+ if (_runtimeBindingId) {
3095
+ const { getSessionBindingService } = await loadSessionBindingRuntime();
3096
+ getSessionBindingService().touch(_runtimeBindingId, eventTs ?? void 0);
3097
+ }
3098
+ const preparedTrigger = isRoom && historyLimit > 0 ? reservedHistorySlot ? roomHistoryTracker.prepareReservedTrigger(_route.agentId, roomId, historyLimit, reservedHistorySlot, {
3099
+ sender: senderName,
3100
+ body: bodyText,
3101
+ timestamp: eventTs ?? void 0,
3102
+ messageId
3103
+ }, historyThreadId) : roomHistoryTracker.prepareTrigger(_route.agentId, roomId, historyLimit, {
3104
+ sender: senderName,
3105
+ body: bodyText,
3106
+ timestamp: eventTs ?? void 0,
3107
+ messageId
3108
+ }, historyThreadId) : void 0;
3109
+ if (reservedHistorySlot && preparedTrigger) markReservedHistorySlotConsumed();
3110
+ return {
3111
+ route: _route,
3112
+ hasExplicitSessionBinding,
3113
+ roomConfig,
3114
+ isDirectMessage,
3115
+ isRoom,
3116
+ shouldRequireMention,
3117
+ wasMentioned,
3118
+ effectiveWasMentioned,
3119
+ shouldBypassMention,
3120
+ canDetectMention,
3121
+ commandAuthorized,
3122
+ inboundHistory: preparedTrigger ? buildInboundHistoryFromEntries({
3123
+ entries: preparedTrigger.history,
3124
+ limit: historyLimit
3125
+ }) : void 0,
3126
+ senderName,
3127
+ bodyText,
3128
+ commandBodyText,
3129
+ media,
3130
+ preflightAudioTranscript,
3131
+ locationPayload,
3132
+ messageId,
3133
+ triggerSnapshot: preparedTrigger,
3134
+ threadRootId,
3135
+ thread,
3136
+ botLoopProtection,
3137
+ effectiveGroupAllowFrom,
3138
+ effectiveRoomUsers
3139
+ };
3140
+ }
3141
+ //#endregion
3142
+ //#region extensions/matrix/src/matrix/monitor/location.ts
3143
+ function decodeGeoUriParamValue(value) {
3144
+ try {
3145
+ return decodeURIComponent(value);
3146
+ } catch {
3147
+ return value;
3148
+ }
3149
+ }
3150
+ function parseGeoUri(value) {
3151
+ const trimmed = value.trim();
3152
+ if (!trimmed) return null;
3153
+ if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("geo:")) return null;
3154
+ const [coordsPart, ...paramParts] = trimmed.slice(4).split(";");
3155
+ if (!coordsPart) return null;
3156
+ const coords = coordsPart.split(",");
3157
+ if (coords.length < 2) return null;
3158
+ const latitude = parseStrictFiniteNumber(coords[0] ?? "");
3159
+ const longitude = parseStrictFiniteNumber(coords[1] ?? "");
3160
+ if (latitude === void 0 || longitude === void 0) return null;
3161
+ const params = /* @__PURE__ */ new Map();
3162
+ for (const part of paramParts) {
3163
+ const segment = part.trim();
3164
+ if (!segment) continue;
3165
+ const eqIndex = segment.indexOf("=");
3166
+ const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
3167
+ const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1);
3168
+ const key = normalizeLowercaseStringOrEmpty(rawKey);
3169
+ if (!key) continue;
3170
+ const valuePart = rawValue.trim();
3171
+ params.set(key, valuePart ? decodeGeoUriParamValue(valuePart) : "");
3172
+ }
3173
+ const accuracyRaw = params.get("u");
3174
+ return {
3175
+ latitude,
3176
+ longitude,
3177
+ accuracy: accuracyRaw ? parseStrictFiniteNumber(accuracyRaw) : void 0
3178
+ };
3179
+ }
3180
+ function resolveMatrixLocation(params) {
3181
+ const { eventType, content } = params;
3182
+ if (!(eventType === EventType.Location || eventType === EventType.RoomMessage && content.msgtype === EventType.Location)) return null;
3183
+ const geoUri = normalizeOptionalString(content.geo_uri) ?? "";
3184
+ if (!geoUri) return null;
3185
+ const parsed = parseGeoUri(geoUri);
3186
+ if (!parsed) return null;
3187
+ const caption = normalizeOptionalString(content.body) ?? "";
3188
+ const location = {
3189
+ latitude: parsed.latitude,
3190
+ longitude: parsed.longitude,
3191
+ accuracy: parsed.accuracy,
3192
+ caption: caption || void 0,
3193
+ source: "pin",
3194
+ isLive: false
3195
+ };
3196
+ return {
3197
+ text: formatLocationText(location),
3198
+ context: toLocationContext(location)
3199
+ };
3200
+ }
3201
+ //#endregion
3202
+ //#region extensions/matrix/src/matrix/monitor/handler-ingress-prefix.ts
3203
+ async function readMatrixIngressPrefix(config) {
3204
+ const { client, senderId, dropPreStartupMessages, eventTs, eventAge, startupMs, startupGraceMs, event, eventType, eventId, inboundDeduper, roomId, logVerboseMessage, directTracker, claimInboundReplay } = config;
3205
+ const selfUserId = await client.getUserId();
3206
+ if (senderId === selfUserId) return;
3207
+ if (dropPreStartupMessages) {
3208
+ if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) return;
3209
+ if (typeof eventTs !== "number" && typeof eventAge === "number" && eventAge > startupGraceMs) return;
3210
+ }
3211
+ const content = event.content;
3212
+ if (eventType === EventType.RoomMessage && isMatrixVerificationRoomMessage({
3213
+ msgtype: content.msgtype,
3214
+ body: content.body
3215
+ })) {
3216
+ logVerboseMessage(`matrix: skip verification/system room message room=${roomId}`);
3217
+ return;
3218
+ }
3219
+ const locationPayload = resolveMatrixLocation({
3220
+ eventType,
3221
+ content
3222
+ });
3223
+ const relates = content["m.relates_to"];
3224
+ if (relates && "rel_type" in relates && relates.rel_type === RelationType.Replace) return;
3225
+ if (hasBundledMatrixReplacementRelation(event)) return;
3226
+ if (eventId && inboundDeduper) {
3227
+ const claim = await inboundDeduper.claim({
3228
+ roomId,
3229
+ eventId
3230
+ });
3231
+ if (claim.kind === "claimed") claimInboundReplay(claim.handle);
3232
+ else if (claim.kind !== "invalid") {
3233
+ logVerboseMessage(`matrix: skip duplicate inbound event room=${roomId} id=${eventId}`);
3234
+ return;
3235
+ }
3236
+ }
3237
+ return {
3238
+ content,
3239
+ isDirectMessage: await directTracker.isDirectMessage({
3240
+ roomId,
3241
+ senderId,
3242
+ selfUserId
3243
+ }),
3244
+ locationPayload,
3245
+ selfUserId
3246
+ };
3247
+ }
3248
+ //#endregion
3249
+ //#region extensions/matrix/src/matrix/monitor/replies.ts
3250
+ const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
3251
+ const THINKING_BLOCK_RE = /<\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
3252
+ function shouldSuppressReasoningReplyText(text) {
3253
+ if (typeof text !== "string") return false;
3254
+ const trimmedStart = text.trimStart();
3255
+ if (!trimmedStart) return false;
3256
+ if (normalizeLowercaseStringOrEmpty(trimmedStart).startsWith("reasoning:")) return true;
3257
+ THINKING_TAG_RE.lastIndex = 0;
3258
+ if (!THINKING_TAG_RE.test(text)) return false;
3259
+ THINKING_BLOCK_RE.lastIndex = 0;
3260
+ const withoutThinkingBlocks = text.replace(THINKING_BLOCK_RE, "");
3261
+ THINKING_TAG_RE.lastIndex = 0;
3262
+ return !withoutThinkingBlocks.replace(THINKING_TAG_RE, "").trim();
3263
+ }
3264
+ async function deliverMatrixReplies(params) {
3265
+ const core = getMatrixRuntime();
3266
+ const tableMode = params.tableMode ?? core.channel.text.resolveMarkdownTableMode({
3267
+ cfg: params.cfg,
3268
+ channel: "matrix",
3269
+ accountId: params.accountId
3270
+ });
3271
+ const logVerbose = (message) => {
3272
+ if (core.logging.shouldLogVerbose()) params.runtime.log?.(message);
3273
+ };
3274
+ let hasReplied = false;
3275
+ let deliveredAny = false;
3276
+ for (const reply of params.replies) {
3277
+ if (reply.isReasoning === true || shouldSuppressReasoningReplyText(reply.text)) {
3278
+ logVerbose("matrix reply suppressed as reasoning-only");
3279
+ continue;
3280
+ }
3281
+ const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
3282
+ if (!reply?.text && !hasMedia) {
3283
+ if (reply?.audioAsVoice) {
3284
+ logVerbose("matrix reply has audioAsVoice without media/text; skipping");
3285
+ continue;
3286
+ }
3287
+ params.runtime.error?.("matrix reply missing text/media");
3288
+ continue;
3289
+ }
3290
+ const replyToIdRaw = (reply.replyToId ?? params.replyToId)?.trim();
3291
+ const replyToId = params.threadId ? replyToIdRaw : params.replyToMode === "off" ? void 0 : replyToIdRaw;
3292
+ const rawText = reply.text ?? "";
3293
+ const mediaList = reply.mediaUrls?.length ? reply.mediaUrls : reply.mediaUrl ? [reply.mediaUrl] : [];
3294
+ const shouldIncludeReply = (id) => Boolean(id) && (params.threadId || params.replyToMode === "all" || !hasReplied);
3295
+ const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : void 0;
3296
+ if (mediaList.length === 0) {
3297
+ let sentTextChunk = false;
3298
+ const { chunks } = chunkMatrixText(rawText, {
3299
+ cfg: params.cfg,
3300
+ accountId: params.accountId,
3301
+ tableMode
3302
+ });
3303
+ for (const chunk of chunks) {
3304
+ const trimmed = chunk.trim();
3305
+ if (!trimmed) continue;
3306
+ await sendMessageMatrix(params.roomId, trimmed, {
3307
+ client: params.client,
3308
+ cfg: params.cfg,
3309
+ replyToId: replyToIdForReply,
3310
+ threadId: params.threadId,
3311
+ accountId: params.accountId
3312
+ });
3313
+ deliveredAny = true;
3314
+ sentTextChunk = true;
3315
+ }
3316
+ if (replyToIdForReply && !hasReplied && sentTextChunk) hasReplied = true;
3317
+ continue;
3318
+ }
3319
+ let first = true;
3320
+ for (const mediaUrl of mediaList) {
3321
+ const caption = first ? rawText : "";
3322
+ await sendMessageMatrix(params.roomId, caption, {
3323
+ client: params.client,
3324
+ cfg: params.cfg,
3325
+ mediaUrl,
3326
+ mediaLocalRoots: params.mediaLocalRoots,
3327
+ replyToId: replyToIdForReply,
3328
+ threadId: params.threadId,
3329
+ audioAsVoice: reply.audioAsVoice,
3330
+ accountId: params.accountId
3331
+ });
3332
+ deliveredAny = true;
3333
+ first = false;
3334
+ }
3335
+ if (replyToIdForReply && !hasReplied) hasReplied = true;
3336
+ }
3337
+ return deliveredAny;
3338
+ }
3339
+ //#endregion
3340
+ //#region extensions/matrix/src/matrix/monitor/handler-reply-dispatcher.ts
3341
+ function createMatrixReplyDispatcher(config) {
3342
+ const { cfg, prefixOptions, humanDelay, typingCallbacks, streaming, draftStream, draftController, client, roomId, runtime, textLimit, replyToMode, threadTarget, replyToEventId, accountId, mediaLocalRoots, tableMode, logVerboseMessage } = config;
3343
+ const quietDraftStreaming = streaming === "quiet" || streaming === "progress";
3344
+ let finalReplyDeliveryFailed = false;
3345
+ let nonFinalReplyDeliveryFailed = false;
3346
+ const { deliver: deliverReply, onError: onReplyError, ...turnDispatcherOptions } = {
3347
+ ...prefixOptions,
3348
+ humanDelay,
3349
+ deliver: async (payload, info) => {
3350
+ if (draftStream && info.kind !== "tool" && !payload.isCompactionNotice) {
3351
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
3352
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
3353
+ const fallbackPayload = ttsSupplement && ttsSupplement.visibleTextAlreadyDelivered !== true && !payload.text?.trim() ? {
3354
+ ...payload,
3355
+ text: ttsSupplement.spokenText
3356
+ } : payload;
3357
+ if (draftController.isDraftConsumed()) {
3358
+ await draftStream.discardPending();
3359
+ await deliverMatrixReplies({
3360
+ cfg,
3361
+ replies: [fallbackPayload],
3362
+ roomId,
3363
+ client,
3364
+ runtime,
3365
+ textLimit,
3366
+ replyToMode,
3367
+ threadId: threadTarget,
3368
+ replyToId: threadTarget ?? replyToEventId ?? void 0,
3369
+ accountId,
3370
+ mediaLocalRoots,
3371
+ tableMode
3372
+ });
3373
+ return;
3374
+ }
3375
+ const payloadReplyToId = normalizeOptionalString(payload.replyToId);
3376
+ const payloadReplyMismatch = replyToMode !== "off" && !threadTarget && payloadReplyToId !== draftController.currentReplyToId();
3377
+ let mustDeliverFinalNormally = draftStream.mustDeliverFinalNormally();
3378
+ if (Boolean(payload.text?.trim()) && !payload.isError && !payloadReplyMismatch && !mustDeliverFinalNormally) {
3379
+ await draftStream.stop();
3380
+ mustDeliverFinalNormally = draftStream.mustDeliverFinalNormally();
3381
+ } else await draftStream.discardPending();
3382
+ const draftEventId = draftStream.eventId();
3383
+ const draftFinalTextNeedsNormalMentionDelivery = Boolean(draftEventId) && typeof payload.text === "string" && Boolean(payload.text.trim()) && !payload.isError && !payloadReplyMismatch && !mustDeliverFinalNormally && await matrixTextWouldActivateMentions(client, payload.text);
3384
+ if (draftEventId && payload.text && !payload.isError && !hasMedia && !payloadReplyMismatch && !mustDeliverFinalNormally && !draftFinalTextNeedsNormalMentionDelivery) {
3385
+ const finalPreviewText = payload.text;
3386
+ await deliverWithFinalizableLivePreviewAdapter({
3387
+ kind: "final",
3388
+ payload,
3389
+ adapter: defineFinalizableLivePreviewAdapter({
3390
+ draft: {
3391
+ flush: async () => {},
3392
+ clear: async () => {},
3393
+ discardPending: async () => {},
3394
+ id: () => draftEventId
3395
+ },
3396
+ buildFinalEdit: () => ({
3397
+ text: finalPreviewText,
3398
+ finalizeLive: !(quietDraftStreaming || !draftStream.matchesPreparedText(finalPreviewText)),
3399
+ ...quietDraftStreaming ? { extraContent: buildMatrixFinalizedPreviewContent() } : {}
3400
+ }),
3401
+ editFinal: async (_draftEventId, edit) => {
3402
+ if (edit.finalizeLive) {
3403
+ if (!await draftStream.finalizeLive()) throw new Error("Matrix draft live finalize failed");
3404
+ return;
3405
+ }
3406
+ const { editMessageMatrix } = await loadMatrixSendModule();
3407
+ await editMessageMatrix(roomId, _draftEventId, edit.text, {
3408
+ client,
3409
+ cfg,
3410
+ threadId: threadTarget,
3411
+ accountId,
3412
+ extraContent: edit.extraContent
3413
+ });
3414
+ },
3415
+ createPreviewReceipt: (id) => createPreviewMessageReceipt({
3416
+ id,
3417
+ ...threadTarget ? { threadId: threadTarget } : {},
3418
+ ...draftController.currentReplyToId() ? { replyToId: draftController.currentReplyToId() } : {}
3419
+ }),
3420
+ logPreviewEditFailure: (err) => {
3421
+ logVerboseMessage(`matrix: preview final edit failed: ${String(err)}`);
3422
+ }
3423
+ }),
3424
+ deliverNormally: async () => {
3425
+ await redactMatrixDraftEvent(client, roomId, draftEventId);
3426
+ await deliverMatrixReplies({
3427
+ cfg,
3428
+ replies: [fallbackPayload],
3429
+ roomId,
3430
+ client,
3431
+ runtime,
3432
+ textLimit,
3433
+ replyToMode,
3434
+ threadId: threadTarget,
3435
+ replyToId: threadTarget ?? replyToEventId ?? void 0,
3436
+ accountId,
3437
+ mediaLocalRoots,
3438
+ tableMode
3439
+ });
3440
+ }
3441
+ });
3442
+ draftController.markDraftConsumed();
3443
+ } else if (draftEventId && hasMedia && !payloadReplyMismatch) {
3444
+ let textEditOk = !mustDeliverFinalNormally;
3445
+ const payloadText = payload.text ?? ttsSupplement?.spokenText;
3446
+ const payloadTextMatchesDraft = typeof payloadText === "string" && draftStream.matchesPreparedText(payloadText);
3447
+ const reusesDraftTextUnchanged = typeof payloadText === "string" && Boolean(payloadText.trim()) && payloadTextMatchesDraft;
3448
+ const mediaTextNeedsNormalMentionDelivery = typeof payloadText === "string" && Boolean(payloadText.trim()) && await matrixTextWouldActivateMentions(client, payloadText);
3449
+ const requiresFinalTextEdit = quietDraftStreaming || typeof payloadText === "string" && !payloadTextMatchesDraft;
3450
+ if (textEditOk && mediaTextNeedsNormalMentionDelivery) textEditOk = false;
3451
+ else if (textEditOk && payloadText && requiresFinalTextEdit) {
3452
+ const { editMessageMatrix } = await loadMatrixSendModule();
3453
+ textEditOk = await editMessageMatrix(roomId, draftEventId, payloadText, {
3454
+ client,
3455
+ cfg,
3456
+ threadId: threadTarget,
3457
+ accountId,
3458
+ extraContent: quietDraftStreaming ? buildMatrixFinalizedPreviewContent() : void 0
3459
+ }).then(() => true, () => false);
3460
+ } else if (textEditOk && reusesDraftTextUnchanged) textEditOk = await draftStream.finalizeLive();
3461
+ const reusesDraftAsFinalText = Boolean(payloadText?.trim()) && textEditOk;
3462
+ if (!reusesDraftAsFinalText) await redactMatrixDraftEvent(client, roomId, draftEventId);
3463
+ const mediaPayload = ttsSupplement && reusesDraftAsFinalText ? buildTtsSupplementMediaPayload(payload) : {
3464
+ ...payload,
3465
+ text: reusesDraftAsFinalText ? void 0 : payload.text ?? (ttsSupplement?.visibleTextAlreadyDelivered === true ? void 0 : ttsSupplement?.spokenText)
3466
+ };
3467
+ await deliverMatrixReplies({
3468
+ cfg,
3469
+ replies: [mediaPayload],
3470
+ roomId,
3471
+ client,
3472
+ runtime,
3473
+ textLimit,
3474
+ replyToMode,
3475
+ threadId: threadTarget,
3476
+ replyToId: threadTarget ?? replyToEventId ?? void 0,
3477
+ accountId,
3478
+ mediaLocalRoots,
3479
+ tableMode
3480
+ });
3481
+ draftController.markDraftConsumed();
3482
+ } else {
3483
+ const draftRedacted = Boolean(draftEventId) && (payload.isError || payloadReplyMismatch || mustDeliverFinalNormally || draftFinalTextNeedsNormalMentionDelivery);
3484
+ if (draftRedacted && draftEventId) await redactMatrixDraftEvent(client, roomId, draftEventId);
3485
+ const deliveredFallback = await deliverMatrixReplies({
3486
+ cfg,
3487
+ replies: [fallbackPayload],
3488
+ roomId,
3489
+ client,
3490
+ runtime,
3491
+ textLimit,
3492
+ replyToMode,
3493
+ threadId: threadTarget,
3494
+ replyToId: threadTarget ?? replyToEventId ?? void 0,
3495
+ accountId,
3496
+ mediaLocalRoots,
3497
+ tableMode
3498
+ });
3499
+ if (draftRedacted || deliveredFallback) draftController.markDraftConsumed();
3500
+ }
3501
+ if (info.kind === "block") {
3502
+ draftController.clearDraftConsumed();
3503
+ draftController.advanceDraftBlockBoundary({ fallbackToLatestEnd: true });
3504
+ draftStream.reset();
3505
+ draftController.resetReplyToIdForNextBlock();
3506
+ draftController.updateDraftFromLatestFullText();
3507
+ const { sendTypingMatrix } = await loadMatrixSendModule();
3508
+ await sendTypingMatrix(roomId, true, void 0, client).catch(() => {});
3509
+ }
3510
+ } else await deliverMatrixReplies({
3511
+ cfg,
3512
+ replies: [payload],
3513
+ roomId,
3514
+ client,
3515
+ runtime,
3516
+ textLimit,
3517
+ replyToMode,
3518
+ threadId: threadTarget,
3519
+ replyToId: threadTarget ?? replyToEventId ?? void 0,
3520
+ accountId,
3521
+ mediaLocalRoots,
3522
+ tableMode
3523
+ });
3524
+ },
3525
+ onError: (err, info) => {
3526
+ if (info.kind === "final") finalReplyDeliveryFailed = true;
3527
+ else nonFinalReplyDeliveryFailed = true;
3528
+ if (info.kind === "block") draftController.advanceDraftBlockBoundary({ fallbackToLatestEnd: true });
3529
+ runtime.error?.(`matrix ${info.kind} reply failed: ${String(err)}`);
3530
+ },
3531
+ onReplyStart: typingCallbacks.onReplyStart,
3532
+ onIdle: typingCallbacks.onIdle
2175
3533
  };
2176
- return async (input) => {
2177
- const cacheKey = `${input.roomId}:${input.threadRootId}`;
2178
- const cached = cache.get(cacheKey);
2179
- if (cached) return cached;
2180
- const rootEvent = await params.client.getEvent(input.roomId, input.threadRootId).catch((err) => {
2181
- params.logVerboseMessage(`matrix: failed resolving thread root room=${input.roomId} id=${input.threadRootId}: ${String(err)}`);
2182
- return null;
2183
- });
2184
- if (!rootEvent) return { threadStarterBody: `Matrix thread root ${input.threadRootId}` };
2185
- const rawEvent = rootEvent;
2186
- const senderId = trimMatrixMaybeString(rawEvent.sender);
2187
- const senderName = senderId && await params.getMemberDisplayName(input.roomId, senderId).catch(() => void 0);
2188
- const senderLabel = senderName ?? senderId;
2189
- const summary = summarizeMatrixThreadStarterEvent(rawEvent);
2190
- return remember(cacheKey, {
2191
- threadStarterBody: formatMatrixThreadStarterBody({
2192
- threadRootId: input.threadRootId,
2193
- senderId,
2194
- senderName,
2195
- summary
2196
- }),
2197
- senderId,
2198
- senderLabel,
2199
- summary
2200
- });
3534
+ return {
3535
+ deliverReply,
3536
+ onReplyError,
3537
+ turnDispatcherOptions,
3538
+ finalReplyDeliveryFailed: () => finalReplyDeliveryFailed,
3539
+ nonFinalReplyDeliveryFailed: () => nonFinalReplyDeliveryFailed
2201
3540
  };
2202
3541
  }
2203
3542
  //#endregion
2204
- //#region extensions/matrix/src/matrix/monitor/handler.ts
3543
+ //#region extensions/matrix/src/matrix/monitor/handler-state.ts
2205
3544
  const ALLOW_FROM_STORE_CACHE_TTL_MS = 3e4;
2206
3545
  const PAIRING_REPLY_COOLDOWN_MS = 5 * 6e4;
2207
- const MATRIX_TOOL_PROGRESS_MAX_CHARS = 300;
2208
- const loadMatrixSendModule = createLazyRuntimeModule(() => import("./send-BJbrdmNm.js").then((n) => n.l));
2209
- const loadAcpBindingRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/acp-binding-runtime"));
2210
- const loadSessionBindingRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/session-binding-runtime"));
2211
- const loadMatrixReactionEvents = createLazyRuntimeModule(() => import("./reaction-events-BwAKtUyQ.js"));
2212
- const loadMatrixDraftStream = createLazyRuntimeModule(() => import("./draft-stream-CTLKtJJ0.js"));
2213
- async function matrixTextWouldActivateMentions(client, text) {
2214
- const { resolveMatrixMentionsForBody } = await loadMatrixSendModule();
2215
- const mentions = await resolveMatrixMentionsForBody({
2216
- client,
2217
- body: text
2218
- });
2219
- return mentions.room === true || (mentions.user_ids?.length ?? 0) > 0;
2220
- }
2221
3546
  const MAX_TRACKED_PAIRING_REPLY_SENDERS = 512;
2222
- const MAX_TRACKED_SHARED_DM_CONTEXT_NOTICES = 512;
2223
- async function redactMatrixDraftEvent(client, roomId, draftEventId) {
2224
- await client.redactEvent(roomId, draftEventId).catch(() => {});
2225
- }
2226
- function buildMatrixFinalizedPreviewContent() {
2227
- return { [MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY]: true };
2228
- }
2229
- function resolveMatrixMentionPrecheckText(params) {
2230
- if (params.locationText?.trim()) return params.locationText.trim();
2231
- if (typeof params.content.body === "string" && params.content.body.trim()) return params.content.body.trim();
2232
- if (isPollStartType(params.eventType)) {
2233
- const parsed = parsePollStartContent(params.content);
2234
- if (parsed) return formatPollAsText(parsed);
2235
- }
2236
- return "";
2237
- }
2238
- function hasBundledMatrixReplacementRelation(event) {
2239
- const relations = event.unsigned?.["m.relations"];
2240
- if (!relations || typeof relations !== "object") return false;
2241
- return relations[RelationType.Replace] !== void 0;
2242
- }
2243
- function resolveMatrixInboundBodyText(params) {
2244
- if (params.mediaPlaceholder) return params.rawBody || params.mediaPlaceholder;
2245
- if (!params.mediaDownloadFailed || !params.hadMediaUrl) return params.rawBody;
2246
- if (params.mediaSizeLimitExceeded) return formatMatrixMediaTooLargeText({
2247
- body: params.rawBody,
2248
- filename: params.filename,
2249
- msgtype: params.msgtype
2250
- });
2251
- return formatMatrixMediaUnavailableText({
2252
- body: params.rawBody,
2253
- filename: params.filename,
2254
- msgtype: params.msgtype
2255
- });
2256
- }
2257
- function markTrackedRoomIfFirst(set, roomId) {
2258
- if (set.has(roomId)) return false;
2259
- set.add(roomId);
2260
- if (set.size > MAX_TRACKED_SHARED_DM_CONTEXT_NOTICES) {
2261
- const oldest = set.keys().next().value;
2262
- if (typeof oldest === "string") set.delete(oldest);
2263
- }
2264
- return true;
2265
- }
2266
- function resolveMatrixSharedDmContextNotice(params) {
2267
- if ((params.dmSessionScope ?? "per-user") === "per-room") return null;
2268
- if (params.sentRooms.has(params.roomId)) return null;
2269
- try {
2270
- const currentSession = resolveMatrixStoredSessionMeta(getSessionEntry({
2271
- storePath: params.storePath,
2272
- sessionKey: params.sessionKey
2273
- }));
2274
- if (!currentSession) return null;
2275
- if (currentSession.channel && currentSession.channel !== "matrix") return null;
2276
- if (currentSession.accountId && currentSession.accountId !== params.accountId) return null;
2277
- if (!currentSession.directUserId) return null;
2278
- if (!currentSession.roomId || currentSession.roomId === params.roomId) return null;
2279
- return [
2280
- "This Matrix DM is sharing a session with another Matrix DM room.",
2281
- "Use /focus here for a one-off isolated thread session when thread bindings are enabled, or set",
2282
- "channels.matrix.dm.sessionScope to per-room to isolate each Matrix DM room."
2283
- ].join(" ");
2284
- } catch (err) {
2285
- params.logVerboseMessage(`matrix: failed checking shared DM session notice room=${params.roomId} (${String(err)})`);
2286
- return null;
2287
- }
2288
- }
2289
- function resolveMatrixPendingHistoryText(params) {
2290
- if (params.mentionPrecheckText) return params.mentionPrecheckText;
2291
- if (!params.mediaUrl) return "";
2292
- const body = typeof params.content.body === "string" ? params.content.body.trim() : void 0;
2293
- const filename = typeof params.content.filename === "string" ? params.content.filename.trim() : void 0;
2294
- const msgtype = typeof params.content.msgtype === "string" ? params.content.msgtype : void 0;
2295
- return formatMatrixMessageText({
2296
- body: resolveMatrixMessageBody({
2297
- body,
2298
- filename,
2299
- msgtype
2300
- }),
2301
- attachment: resolveMatrixMessageAttachment({
2302
- body,
2303
- filename,
2304
- msgtype
2305
- })
2306
- }) ?? "";
2307
- }
2308
- function isMatrixAudioMediaEnabled(cfg) {
2309
- return cfg.tools?.media?.audio?.enabled !== false;
2310
- }
2311
- function shouldDeferMatrixAudioPreflightForRoomIngress(params) {
2312
- if (!isMatrixAudioMediaEnabled(params.cfg)) return false;
2313
- const content = params.content;
2314
- const contentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
2315
- const contentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
2316
- const mediaUrl = contentUrl ?? contentFile?.url;
2317
- const contentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
2318
- return mediaUrl?.startsWith("mxc://") === true && isMatrixAudioContent({
2319
- msgtype: typeof content.msgtype === "string" ? content.msgtype : void 0,
2320
- mimetype: contentInfo?.mimetype
2321
- });
2322
- }
2323
- function resolveMatrixAllowBotsMode(value) {
2324
- if (value === true) return "all";
2325
- if (value === "mentions") return "mentions";
2326
- return "off";
2327
- }
2328
- function formatMatrixToolProgressMarkdownCode(text) {
2329
- return `\`${(text.length <= MATRIX_TOOL_PROGRESS_MAX_CHARS ? text : `${truncateUtf16Safe(text, MATRIX_TOOL_PROGRESS_MAX_CHARS - 1).trimEnd()}...`).replaceAll("`", "'")}\``;
2330
- }
2331
- function createMatrixRoomMessageHandler(params) {
2332
- const { client, core, cfg, accountId, accountConfig, runtime, logger, logVerboseMessage, allowFromResolvedEntries = [], groupAllowFromResolvedEntries = [], roomsConfig, accountAllowBots, configuredBotUserIds = /* @__PURE__ */ new Set(), groupPolicy, replyToMode, threadReplies, dmThreadReplies, dmSessionScope, streaming, previewToolProgressEnabled, blockStreamingEnabled, dmEnabled, dmPolicy, textLimit, mediaMaxBytes, historyLimit, startupMs, startupGraceMs, dropPreStartupMessages, inboundDeduper, directTracker, getRoomInfo, getMemberDisplayName, needsRoomAliasesForConfig, resolveLiveUserAllowlist = resolveMatrixMonitorLiveUserAllowlist, resolveStorePath: resolveStorePathImpl = resolveStorePath, createChannelInboundEnvelopeBuilder: createChannelInboundEnvelopeBuilderImpl = createChannelInboundEnvelopeBuilder, finalizeInboundContext, resolveHumanDelayConfig: resolveHumanDelayConfigImpl = resolveHumanDelayConfig } = params;
2333
- const contextVisibilityMode = resolveChannelContextVisibilityMode({
2334
- cfg,
2335
- channel: "matrix",
2336
- accountId
2337
- });
3547
+ function createMatrixHandlerState(config) {
3548
+ const { core, accountId, runtime, allowFromResolvedEntries, groupAllowFromResolvedEntries, resolveLiveUserAllowlist } = config;
2338
3549
  let cachedStoreAllowFrom = null;
2339
3550
  let liveDmAllowlistCache = null;
2340
3551
  let liveGroupAllowlistCache = null;
@@ -2365,19 +3576,35 @@ function createMatrixRoomMessageHandler(params) {
2365
3576
  return entries;
2366
3577
  };
2367
3578
  const pairingReplySentAtMsBySender = /* @__PURE__ */ new Map();
2368
- const resolveThreadContext = createMatrixThreadContextResolver({
2369
- client,
2370
- getMemberDisplayName,
2371
- logVerboseMessage
2372
- });
2373
- const resolveReplyContext = createMatrixReplyContextResolver({
2374
- client,
2375
- getMemberDisplayName,
2376
- logVerboseMessage
2377
- });
2378
- const roomHistoryTracker = createRoomHistoryTracker();
2379
- const roomIngressQueue = new KeyedAsyncQueue();
2380
- const sharedDmContextNoticeRooms = /* @__PURE__ */ new Set();
3579
+ const resolveLiveAccountAllowlists = async () => {
3580
+ const liveCfg = core.config.current();
3581
+ const liveAccountAllowlists = resolveMatrixAccountAllowlistConfig({
3582
+ cfg: liveCfg,
3583
+ accountId
3584
+ });
3585
+ return {
3586
+ liveCfg,
3587
+ liveDmAllowFrom: await resolveCachedLiveAllowlist({
3588
+ cfg: liveCfg,
3589
+ entries: liveAccountAllowlists.dmAllowFrom,
3590
+ startupResolvedEntries: allowFromResolvedEntries,
3591
+ cache: liveDmAllowlistCache,
3592
+ updateCache: (next) => {
3593
+ liveDmAllowlistCache = next;
3594
+ }
3595
+ }),
3596
+ liveGroupAllowFrom: await resolveCachedLiveAllowlist({
3597
+ cfg: liveCfg,
3598
+ entries: liveAccountAllowlists.groupAllowFrom,
3599
+ failClosedOnUnresolved: true,
3600
+ startupResolvedEntries: groupAllowFromResolvedEntries,
3601
+ cache: liveGroupAllowlistCache,
3602
+ updateCache: (next) => {
3603
+ liveGroupAllowlistCache = next;
3604
+ }
3605
+ })
3606
+ };
3607
+ };
2381
3608
  const readStoreAllowFrom = async () => {
2382
3609
  const now = Date.now();
2383
3610
  if (cachedStoreAllowFrom && isFutureDateTimestampMs(cachedStoreAllowFrom.expiresAtMs, { nowMs: now })) return cachedStoreAllowFrom.value;
@@ -2409,14 +3636,59 @@ function createMatrixRoomMessageHandler(params) {
2409
3636
  }
2410
3637
  return true;
2411
3638
  };
3639
+ return {
3640
+ resolveLiveAccountAllowlists,
3641
+ readStoreAllowFrom,
3642
+ shouldSendPairingReply
3643
+ };
3644
+ }
3645
+ //#endregion
3646
+ //#region extensions/matrix/src/matrix/monitor/handler.ts
3647
+ function createMatrixRoomMessageHandler(params) {
3648
+ const { client, core, cfg, accountId, runtime, logger, logVerboseMessage, allowFromResolvedEntries = [], groupAllowFromResolvedEntries = [], configuredBotUserIds = /* @__PURE__ */ new Set(), groupPolicy, replyToMode, dmSessionScope, streaming, previewToolProgressEnabled, blockStreamingEnabled, textLimit, historyLimit, startupMs, startupGraceMs, dropPreStartupMessages, inboundDeduper, directTracker, getRoomInfo, getMemberDisplayName, resolveLiveUserAllowlist = resolveMatrixMonitorLiveUserAllowlist, resolveStorePath: resolveStorePathImpl = resolveStorePath, createChannelInboundEnvelopeBuilder: createChannelInboundEnvelopeBuilderImpl = createChannelInboundEnvelopeBuilder, finalizeInboundContext, resolveHumanDelayConfig: resolveHumanDelayConfigImpl = resolveHumanDelayConfig } = params;
3649
+ const handlerConfig = {
3650
+ ...params,
3651
+ allowFromResolvedEntries,
3652
+ groupAllowFromResolvedEntries,
3653
+ configuredBotUserIds,
3654
+ resolveLiveUserAllowlist,
3655
+ resolveStorePath: resolveStorePathImpl,
3656
+ createChannelInboundEnvelopeBuilder: createChannelInboundEnvelopeBuilderImpl,
3657
+ resolveHumanDelayConfig: resolveHumanDelayConfigImpl
3658
+ };
3659
+ const contextVisibilityMode = resolveChannelContextVisibilityMode({
3660
+ cfg,
3661
+ channel: "matrix",
3662
+ accountId
3663
+ });
3664
+ const handlerState = createMatrixHandlerState({
3665
+ core,
3666
+ accountId,
3667
+ runtime,
3668
+ allowFromResolvedEntries,
3669
+ groupAllowFromResolvedEntries,
3670
+ resolveLiveUserAllowlist
3671
+ });
3672
+ const resolveThreadContext = createMatrixThreadContextResolver({
3673
+ client,
3674
+ getMemberDisplayName,
3675
+ logVerboseMessage
3676
+ });
3677
+ const resolveReplyContext = createMatrixReplyContextResolver({
3678
+ client,
3679
+ getMemberDisplayName,
3680
+ logVerboseMessage
3681
+ });
3682
+ const roomHistoryTracker = createRoomHistoryTracker();
3683
+ const roomIngressQueue = new KeyedAsyncQueue();
3684
+ const sharedDmContextNoticeRooms = /* @__PURE__ */ new Set();
2412
3685
  const runRoomIngress = async (roomId, task) => {
2413
3686
  return await roomIngressQueue.enqueue(roomId, task);
2414
3687
  };
2415
3688
  return async (roomId, event) => {
2416
3689
  const eventId = typeof event.event_id === "string" ? event.event_id.trim() : "";
2417
3690
  let inboundReplayClaim;
2418
- let draftStreamRef;
2419
- let draftConsumed = false;
3691
+ let draftControllerRef;
2420
3692
  try {
2421
3693
  const eventType = event.type;
2422
3694
  if (eventType === EventType.RoomMessageEncrypted) return;
@@ -2436,563 +3708,54 @@ function createMatrixRoomMessageHandler(params) {
2436
3708
  await inboundReplayClaim.commit();
2437
3709
  inboundReplayClaim = void 0;
2438
3710
  };
2439
- const readIngressPrefix = async () => {
2440
- const selfUserId = await client.getUserId();
2441
- if (senderId === selfUserId) return;
2442
- if (dropPreStartupMessages) {
2443
- if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) return;
2444
- if (typeof eventTs !== "number" && typeof eventAge === "number" && eventAge > startupGraceMs) return;
2445
- }
2446
- const content = event.content;
2447
- if (eventType === EventType.RoomMessage && isMatrixVerificationRoomMessage({
2448
- msgtype: content.msgtype,
2449
- body: content.body
2450
- })) {
2451
- logVerboseMessage(`matrix: skip verification/system room message room=${roomId}`);
2452
- return;
2453
- }
2454
- const locationPayload = resolveMatrixLocation({
2455
- eventType,
2456
- content
2457
- });
2458
- const relates = content["m.relates_to"];
2459
- if (relates && "rel_type" in relates && relates.rel_type === RelationType.Replace) return;
2460
- if (hasBundledMatrixReplacementRelation(event)) return;
2461
- if (eventId && inboundDeduper) {
2462
- const claim = await inboundDeduper.claim({
2463
- roomId,
2464
- eventId
2465
- });
2466
- if (claim.kind === "claimed") inboundReplayClaim = claim.handle;
2467
- else if (claim.kind !== "invalid") {
2468
- logVerboseMessage(`matrix: skip duplicate inbound event room=${roomId} id=${eventId}`);
2469
- return;
2470
- }
2471
- }
2472
- return {
2473
- content,
2474
- isDirectMessage: await directTracker.isDirectMessage({
2475
- roomId,
2476
- senderId,
2477
- selfUserId
2478
- }),
2479
- locationPayload,
2480
- selfUserId
2481
- };
2482
- };
2483
- const continueIngress = async (paramsLocal) => {
2484
- let content = paramsLocal.content;
2485
- const isDirectMessage = paramsLocal.isDirectMessage;
2486
- const isRoom = !isDirectMessage;
2487
- const { audioPreflightMode, locationPayload, reservedHistorySlot, selfUserId } = paramsLocal;
2488
- const messageId = event.event_id ?? "";
2489
- const threadRootId = resolveMatrixThreadRootId({
2490
- event,
2491
- content
2492
- });
2493
- const thread = resolveMatrixThreadRouting({
2494
- isDirectMessage,
2495
- threadReplies,
2496
- dmThreadReplies,
2497
- messageId,
2498
- threadRootId
2499
- });
2500
- const historyThreadId = threadRootId ? thread.threadId : void 0;
2501
- let reservedHistorySlotConsumed = false;
2502
- const discardReservedHistorySlot = () => {
2503
- if (reservedHistorySlot && !reservedHistorySlotConsumed) {
2504
- roomHistoryTracker.discardPending(roomId, reservedHistorySlot, historyThreadId);
2505
- reservedHistorySlotConsumed = true;
2506
- }
2507
- };
2508
- const commitInboundEventIfClaimedAndDiscardReserved = async () => {
2509
- discardReservedHistorySlot();
2510
- await commitInboundEventIfClaimed();
2511
- };
2512
- if (isRoom && groupPolicy === "disabled") {
2513
- await commitInboundEventIfClaimedAndDiscardReserved();
2514
- return;
2515
- }
2516
- const roomInfoForConfig = isRoom && needsRoomAliasesForConfig ? await getRoomInfo(roomId, { includeAliases: true }) : void 0;
2517
- const roomAliasesForConfig = roomInfoForConfig ? [roomInfoForConfig.canonicalAlias ?? "", ...roomInfoForConfig.altAliases].filter(Boolean) : [];
2518
- const roomConfigInfo = isRoom ? resolveMatrixRoomConfig({
2519
- rooms: roomsConfig,
2520
- roomId,
2521
- aliases: roomAliasesForConfig
2522
- }) : void 0;
2523
- const roomConfig = roomConfigInfo?.config;
2524
- const allowBotsMode = resolveMatrixAllowBotsMode(roomConfig?.allowBots ?? accountAllowBots);
2525
- const isConfiguredBotSender = configuredBotUserIds.has(senderId);
2526
- const roomMatchMeta = roomConfigInfo ? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${roomConfigInfo.matchSource ?? "none"}` : "matchKey=none matchSource=none";
2527
- if (isConfiguredBotSender && allowBotsMode === "off") {
2528
- logVerboseMessage(`matrix: drop configured bot sender=${senderId} (allowBots=false${isDirectMessage ? "" : `, ${roomMatchMeta}`})`);
2529
- await commitInboundEventIfClaimedAndDiscardReserved();
2530
- return;
2531
- }
2532
- const botLoopProtection = isConfiguredBotSender && senderId !== selfUserId ? {
2533
- scopeId: accountId,
2534
- conversationId: roomId,
2535
- senderId,
2536
- receiverId: selfUserId,
2537
- config: mergePairLoopGuardConfig(accountConfig?.botLoopProtection, roomConfig?.botLoopProtection),
2538
- defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
2539
- defaultEnabled: true,
2540
- nowMs: eventTs ?? void 0
2541
- } : void 0;
2542
- if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
2543
- logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
2544
- await commitInboundEventIfClaimedAndDiscardReserved();
2545
- return;
2546
- }
2547
- if (isRoom && groupPolicy === "allowlist") {
2548
- if (!roomConfigInfo?.allowlistConfigured) {
2549
- logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
2550
- await commitInboundEventIfClaimedAndDiscardReserved();
2551
- return;
2552
- }
2553
- if (!roomConfig) {
2554
- logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
2555
- await commitInboundEventIfClaimedAndDiscardReserved();
2556
- return;
2557
- }
3711
+ const readIngressPrefix = () => readMatrixIngressPrefix({
3712
+ client,
3713
+ senderId,
3714
+ dropPreStartupMessages,
3715
+ eventTs: eventTs ?? void 0,
3716
+ eventAge: eventAge ?? void 0,
3717
+ startupMs,
3718
+ startupGraceMs,
3719
+ event,
3720
+ eventType,
3721
+ eventId,
3722
+ inboundDeduper,
3723
+ roomId,
3724
+ logVerboseMessage,
3725
+ directTracker,
3726
+ claimInboundReplay: (handle) => {
3727
+ inboundReplayClaim = handle;
2558
3728
  }
2559
- let senderNamePromise = null;
2560
- const getSenderName = async () => {
2561
- senderNamePromise ??= getMemberDisplayName(roomId, senderId).catch(() => senderId);
2562
- return await senderNamePromise;
2563
- };
2564
- const storeAllowFrom = isDirectMessage && dmPolicy !== "allowlist" && dmPolicy !== "open" ? await readStoreAllowFrom() : [];
2565
- const roomUsers = roomConfig?.users ?? [];
2566
- const liveCfg = core.config.current();
2567
- const liveAccountAllowlists = resolveMatrixAccountAllowlistConfig({
2568
- cfg: liveCfg,
2569
- accountId
2570
- });
2571
- const liveDmAllowFrom = await resolveCachedLiveAllowlist({
2572
- cfg: liveCfg,
2573
- entries: liveAccountAllowlists.dmAllowFrom,
2574
- startupResolvedEntries: allowFromResolvedEntries,
2575
- cache: liveDmAllowlistCache,
2576
- updateCache: (next) => {
2577
- liveDmAllowlistCache = next;
2578
- }
2579
- });
2580
- const liveGroupAllowFrom = await resolveCachedLiveAllowlist({
2581
- cfg: liveCfg,
2582
- entries: liveAccountAllowlists.groupAllowFrom,
2583
- failClosedOnUnresolved: true,
2584
- startupResolvedEntries: groupAllowFromResolvedEntries,
2585
- cache: liveGroupAllowlistCache,
2586
- updateCache: (next) => {
2587
- liveGroupAllowlistCache = next;
2588
- }
2589
- });
2590
- const accessState = await resolveMatrixMonitorAccessState({
2591
- allowFrom: liveDmAllowFrom,
2592
- storeAllowFrom,
2593
- dmPolicy,
2594
- groupPolicy,
2595
- groupAllowFrom: liveGroupAllowFrom,
2596
- roomUsers,
3729
+ });
3730
+ const continueIngress = async (paramsLocal) => {
3731
+ const access = await resolveMatrixIngressAccess({
3732
+ handler: handlerConfig,
3733
+ params: paramsLocal,
3734
+ roomId,
3735
+ event,
3736
+ eventTs: eventTs ?? void 0,
2597
3737
  senderId,
2598
- isRoom,
2599
- accountId,
2600
- eventKind: isReactionEvent ? "reaction" : "message"
2601
- });
2602
- const { effectiveGroupAllowFrom, effectiveRoomUsers, messageIngress } = accessState;
2603
- const ingressDecision = messageIngress.ingress;
2604
- if (isDirectMessage) {
2605
- if (!dmEnabled || dmPolicy === "disabled") {
2606
- await commitInboundEventIfClaimedAndDiscardReserved();
2607
- return;
2608
- }
2609
- const senderReason = messageIngress.senderAccess.reasonCode;
2610
- if (ingressDecision.decision !== "allow") {
2611
- if (ingressDecision.admission === "pairing-required") {
2612
- const senderName = await getSenderName();
2613
- const { code, created } = await core.channel.pairing.upsertPairingRequest({
2614
- channel: "matrix",
2615
- id: senderId,
2616
- accountId,
2617
- meta: { name: senderName }
2618
- });
2619
- if (shouldSendPairingReply(senderId, created)) {
2620
- const pairingReply = core.channel.pairing.buildPairingReply({
2621
- channel: "matrix",
2622
- idLine: `Your Matrix user id: ${senderId}`,
2623
- code
2624
- });
2625
- logVerboseMessage(created ? `matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (reason=${senderReason})` : `matrix pairing reminder sender=${senderId} name=${senderName ?? "unknown"} (reason=${senderReason})`);
2626
- try {
2627
- const { sendMessageMatrix } = await loadMatrixSendModule();
2628
- await sendMessageMatrix(`room:${roomId}`, created ? pairingReply : `${pairingReply}\n\nPairing request is still pending approval. Reusing existing code.`, {
2629
- client,
2630
- cfg,
2631
- accountId
2632
- });
2633
- await commitInboundEventIfClaimed();
2634
- } catch (err) {
2635
- logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
2636
- discardReservedHistorySlot();
2637
- return;
2638
- }
2639
- } else {
2640
- logVerboseMessage(`matrix pairing reminder suppressed sender=${senderId} (cooldown)`);
2641
- await commitInboundEventIfClaimedAndDiscardReserved();
2642
- }
2643
- }
2644
- if (isReactionEvent || dmPolicy !== "pairing") {
2645
- logVerboseMessage(`matrix: blocked ${isReactionEvent ? "reaction" : "dm"} sender ${senderId} (dmPolicy=${dmPolicy}, reason=${senderReason})`);
2646
- await commitInboundEventIfClaimedAndDiscardReserved();
2647
- }
2648
- return;
2649
- }
2650
- }
2651
- if (isRoom && ingressDecision.decision !== "allow") {
2652
- logVerboseMessage(`matrix: blocked sender ${senderId} (ingress=${ingressDecision.reasonCode}, ${roomMatchMeta})`);
2653
- await commitInboundEventIfClaimedAndDiscardReserved();
2654
- return;
2655
- }
2656
- if (isRoom) logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`);
2657
- if (isReactionEvent) {
2658
- const senderName = await getSenderName();
2659
- const { handleInboundMatrixReaction } = await loadMatrixReactionEvents();
2660
- await handleInboundMatrixReaction({
2661
- client,
2662
- core,
2663
- cfg,
2664
- accountId,
2665
- roomId,
2666
- event,
2667
- senderId,
2668
- senderLabel: senderName,
2669
- selfUserId,
2670
- isDirectMessage,
2671
- logVerboseMessage
2672
- });
2673
- await commitInboundEventIfClaimedAndDiscardReserved();
2674
- return;
2675
- }
2676
- let pollSnapshotPromise = null;
2677
- const getPollSnapshot = async () => {
2678
- if (!isPollEvent) return null;
2679
- pollSnapshotPromise ??= fetchMatrixPollSnapshot(client, roomId, event).catch((err) => {
2680
- logVerboseMessage(`matrix: failed resolving poll snapshot room=${roomId} id=${event.event_id ?? "unknown"}: ${String(err)}`);
2681
- return null;
2682
- });
2683
- return await pollSnapshotPromise;
2684
- };
2685
- const mentionPrecheckText = resolveMatrixMentionPrecheckText({
2686
- eventType,
2687
- content,
2688
- locationText: locationPayload?.text
2689
- });
2690
- const contentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
2691
- const contentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
2692
- const mediaUrl = contentUrl ?? contentFile?.url;
2693
- const earlyContentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
2694
- const earlyContentType = earlyContentInfo?.mimetype;
2695
- const earlyContentSize = typeof earlyContentInfo?.size === "number" ? earlyContentInfo.size : void 0;
2696
- const earlyContentBody = typeof content.body === "string" ? content.body.trim() : "";
2697
- const earlyOriginalFilename = (typeof content.filename === "string" ? content.filename.trim() : "") || earlyContentBody || void 0;
2698
- const pendingHistoryText = resolveMatrixPendingHistoryText({
2699
- mentionPrecheckText,
2700
- content,
2701
- mediaUrl
3738
+ isReactionEvent,
3739
+ readStoreAllowFrom: handlerState.readStoreAllowFrom,
3740
+ shouldSendPairingReply: handlerState.shouldSendPairingReply,
3741
+ resolveLiveAccountAllowlists: handlerState.resolveLiveAccountAllowlists,
3742
+ roomHistoryTracker,
3743
+ commitInboundEventIfClaimed
2702
3744
  });
2703
- const pendingHistoryPollText = !pendingHistoryText && isPollEvent && historyLimit > 0 ? (await getPollSnapshot())?.text : "";
2704
- if (!mentionPrecheckText && !mediaUrl && !isPollEvent) {
2705
- await commitInboundEventIfClaimedAndDiscardReserved();
2706
- return;
2707
- }
2708
- let preflightMedia = null;
2709
- let preflightMediaDownloadFailed = false;
2710
- let preflightMediaSizeLimitExceeded = false;
2711
- let preflightAudioTranscript;
2712
- const { route: _route, configuredBinding: _configuredBinding, runtimeBindingId: _runtimeBindingId } = resolveMatrixInboundRoute({
2713
- cfg,
2714
- accountId,
3745
+ if (!access) return;
3746
+ return await resolveMatrixIngressContent({
3747
+ handler: handlerConfig,
3748
+ params: paramsLocal,
3749
+ access,
2715
3750
  roomId,
2716
- senderId,
2717
- isDirectMessage,
2718
- dmSessionScope,
2719
- threadId: thread.threadId,
3751
+ event,
3752
+ eventType,
3753
+ isPollEvent,
2720
3754
  eventTs: eventTs ?? void 0,
2721
- resolveAgentRoute: core.channel.routing.resolveAgentRoute
2722
- });
2723
- const hasExplicitSessionBinding = _configuredBinding !== null || _runtimeBindingId !== null;
2724
- const preflightAudioMediaUrl = mediaUrl?.startsWith("mxc://") ? mediaUrl : void 0;
2725
- const shouldRunMatrixAudioPreflight = isMatrixAudioContent({
2726
- msgtype: typeof content.msgtype === "string" ? content.msgtype : void 0,
2727
- mimetype: earlyContentType
2728
- }) && isMatrixAudioMediaEnabled(cfg) && preflightAudioMediaUrl !== void 0;
2729
- if (shouldRunMatrixAudioPreflight && audioPreflightMode === "defer" && isRoom && historyLimit > 0 && !reservedHistorySlot) {
2730
- const reserved = roomHistoryTracker.reservePending(_route.agentId, roomId, {
2731
- sender: senderId,
2732
- body: pendingHistoryText,
2733
- timestamp: eventTs ?? void 0,
2734
- messageId
2735
- }, historyThreadId);
2736
- return { deferredPrefix: {
2737
- ...paramsLocal,
2738
- audioPreflightMode: "run",
2739
- reservedHistorySlot: reserved
2740
- } };
2741
- }
2742
- if (shouldRunMatrixAudioPreflight) {
2743
- try {
2744
- preflightMedia = await downloadMatrixMedia({
2745
- client,
2746
- mxcUrl: preflightAudioMediaUrl,
2747
- contentType: earlyContentType,
2748
- sizeBytes: earlyContentSize,
2749
- maxBytes: mediaMaxBytes,
2750
- file: contentFile,
2751
- originalFilename: earlyOriginalFilename
2752
- });
2753
- } catch (err) {
2754
- preflightMediaDownloadFailed = true;
2755
- if (isMatrixMediaSizeLimitError(err)) preflightMediaSizeLimitExceeded = true;
2756
- const errorText = formatMatrixErrorMessage(err);
2757
- logVerboseMessage(`matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`);
2758
- logger.warn("matrix media download failed", {
2759
- roomId,
2760
- eventId: event.event_id,
2761
- msgtype: content.msgtype,
2762
- encrypted: Boolean(contentFile),
2763
- error: errorText
2764
- });
2765
- }
2766
- if (preflightMedia) preflightAudioTranscript = await resolveMatrixPreflightAudioTranscript({
2767
- mediaPath: preflightMedia.path,
2768
- mediaContentType: preflightMedia.contentType,
2769
- cfg,
2770
- accountId,
2771
- chatType: isDirectMessage ? "direct" : "channel",
2772
- originatingTo: `room:${roomId}`,
2773
- messageThreadId: thread.threadId,
2774
- sessionKey: _route.sessionKey
2775
- });
2776
- }
2777
- const agentMentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, _route.agentId, {
2778
- provider: "matrix",
2779
- conversationId: roomId,
2780
- providerPolicy: accountConfig?.mentionPatterns
2781
- });
2782
- const selfDisplayName = content.formatted_body ? await getMemberDisplayName(roomId, selfUserId).catch(() => void 0) : void 0;
2783
- const mentionPrecheckTextWithTranscript = preflightAudioTranscript ? [mentionPrecheckText, preflightAudioTranscript].filter(Boolean).join("\n").trim() : mentionPrecheckText;
2784
- const { wasMentioned, hasExplicitMention } = resolveMentions({
2785
- content,
2786
- userId: selfUserId,
2787
- displayName: selfDisplayName,
2788
- text: mentionPrecheckTextWithTranscript,
2789
- mentionRegexes: agentMentionRegexes
2790
- });
2791
- if (isConfiguredBotSender && allowBotsMode === "mentions" && !isDirectMessage && !wasMentioned) {
2792
- logVerboseMessage(`matrix: drop configured bot sender=${senderId} (allowBots=mentions, missing mention, ${roomMatchMeta})`);
2793
- await commitInboundEventIfClaimedAndDiscardReserved();
2794
- return;
2795
- }
2796
- const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
2797
- cfg,
2798
- surface: "matrix"
2799
- });
2800
- const useAccessGroups = cfg.commands?.useAccessGroups !== false;
2801
- const commandCheckText = stripMatrixMentionPrefix({
2802
- text: mentionPrecheckText,
2803
- userId: selfUserId,
2804
- displayName: selfDisplayName,
2805
- mentionRegexes: agentMentionRegexes
2806
- });
2807
- const hasControlCommandInMessage = core.channel.text.hasControlCommand(commandCheckText, cfg);
2808
- const commandAccess = await resolveMatrixMonitorCommandAccess(accessState, {
2809
- useAccessGroups,
2810
- allowTextCommands,
2811
- hasControlCommand: hasControlCommandInMessage
2812
- });
2813
- const commandAuthorized = commandAccess.authorized;
2814
- if (isRoom && commandAccess.shouldBlockControlCommand) {
2815
- logInboundDrop({
2816
- log: logVerboseMessage,
2817
- channel: "matrix",
2818
- reason: "control command (unauthorized)",
2819
- target: senderId
2820
- });
2821
- await commitInboundEventIfClaimedAndDiscardReserved();
2822
- return;
2823
- }
2824
- const shouldRequireMention = isRoom ? roomConfig?.autoReply === true ? false : roomConfig?.autoReply === false ? true : typeof roomConfig?.requireMention === "boolean" ? roomConfig?.requireMention : true : false;
2825
- const mentionDecision = resolveInboundMentionDecision({
2826
- facts: {
2827
- canDetectMention: true,
2828
- wasMentioned,
2829
- hasAnyMention: hasExplicitMention
2830
- },
2831
- policy: {
2832
- isGroup: isRoom,
2833
- requireMention: shouldRequireMention,
2834
- allowTextCommands,
2835
- hasControlCommand: hasControlCommandInMessage,
2836
- commandAuthorized
2837
- }
2838
- });
2839
- const { effectiveWasMentioned, shouldBypassMention } = mentionDecision;
2840
- const canDetectMention = agentMentionRegexes.length > 0 || hasExplicitMention;
2841
- if (mentionDecision.shouldSkip) {
2842
- const pendingHistoryBody = preflightAudioTranscript ? formatMatrixAudioTranscript(preflightAudioTranscript) : pendingHistoryText || pendingHistoryPollText;
2843
- if (historyLimit > 0 && pendingHistoryBody) {
2844
- const pendingEntry = {
2845
- sender: senderId,
2846
- body: pendingHistoryBody,
2847
- timestamp: eventTs ?? void 0,
2848
- messageId
2849
- };
2850
- if (reservedHistorySlot) {
2851
- roomHistoryTracker.finalizePending(roomId, reservedHistorySlot, pendingEntry, historyThreadId);
2852
- reservedHistorySlotConsumed = true;
2853
- } else roomHistoryTracker.recordPending(roomId, pendingEntry, historyThreadId);
2854
- }
2855
- logger.info("skipping room message", {
2856
- roomId,
2857
- reason: "no-mention"
2858
- });
2859
- await commitInboundEventIfClaimed();
2860
- return;
2861
- }
2862
- if (preflightAudioTranscript) await sendMatrixPreflightAudioTranscriptEcho({
2863
- transcript: preflightAudioTranscript,
2864
- cfg,
2865
- accountId,
2866
- originatingTo: `room:${roomId}`,
2867
- messageThreadId: thread.threadId
2868
- });
2869
- if (isPollEvent) {
2870
- const pollSnapshot = await getPollSnapshot();
2871
- if (!pollSnapshot) {
2872
- discardReservedHistorySlot();
2873
- return;
2874
- }
2875
- content = {
2876
- msgtype: "m.text",
2877
- body: pollSnapshot.text
2878
- };
2879
- }
2880
- let media = preflightMedia;
2881
- let mediaDownloadFailed = preflightMediaDownloadFailed;
2882
- let mediaSizeLimitExceeded = preflightMediaSizeLimitExceeded;
2883
- const finalContentUrl = "url" in content && typeof content.url === "string" ? content.url : void 0;
2884
- const finalContentFile = "file" in content && content.file && typeof content.file === "object" ? content.file : void 0;
2885
- const finalMediaUrl = finalContentUrl ?? finalContentFile?.url;
2886
- const contentBody = typeof content.body === "string" ? content.body.trim() : "";
2887
- const originalFilename = (typeof content.filename === "string" ? content.filename.trim() : "") || contentBody || void 0;
2888
- const contentInfo = "info" in content && content.info && typeof content.info === "object" ? content.info : void 0;
2889
- const contentType = contentInfo?.mimetype;
2890
- const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : void 0;
2891
- if (!media && !mediaDownloadFailed && finalMediaUrl?.startsWith("mxc://")) try {
2892
- media = await downloadMatrixMedia({
2893
- client,
2894
- mxcUrl: finalMediaUrl,
2895
- contentType,
2896
- sizeBytes: contentSize,
2897
- maxBytes: mediaMaxBytes,
2898
- file: finalContentFile,
2899
- originalFilename
2900
- });
2901
- } catch (err) {
2902
- mediaDownloadFailed = true;
2903
- if (isMatrixMediaSizeLimitError(err)) mediaSizeLimitExceeded = true;
2904
- const errorText = formatMatrixErrorMessage(err);
2905
- logVerboseMessage(`matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`);
2906
- logger.warn("matrix media download failed", {
2907
- roomId,
2908
- eventId: event.event_id,
2909
- msgtype: content.msgtype,
2910
- encrypted: Boolean(finalContentFile),
2911
- error: errorText
2912
- });
2913
- }
2914
- let bodyText = resolveMatrixInboundBodyText({
2915
- rawBody: locationPayload?.text ?? contentBody,
2916
- filename: typeof content.filename === "string" ? content.filename : void 0,
2917
- mediaPlaceholder: media?.placeholder,
2918
- msgtype: content.msgtype,
2919
- hadMediaUrl: Boolean(finalMediaUrl),
2920
- mediaDownloadFailed,
2921
- mediaSizeLimitExceeded
3755
+ senderId,
3756
+ roomHistoryTracker,
3757
+ commitInboundEventIfClaimed
2922
3758
  });
2923
- if (preflightMedia && bodyText && bodyText !== preflightMedia.placeholder && isLikelyBareFilename(bodyText)) bodyText = preflightMedia.placeholder;
2924
- if (preflightAudioTranscript) {
2925
- const transcriptBody = formatMatrixAudioTranscript(preflightAudioTranscript);
2926
- bodyText = !bodyText || bodyText === media?.placeholder ? transcriptBody : `${bodyText}\n${transcriptBody}`;
2927
- }
2928
- if (!bodyText) {
2929
- await commitInboundEventIfClaimedAndDiscardReserved();
2930
- return;
2931
- }
2932
- const commandBodyText = hasControlCommandInMessage ? commandCheckText : bodyText;
2933
- const senderName = await getSenderName();
2934
- if (_configuredBinding) {
2935
- const { ensureConfiguredAcpBindingReady } = await loadAcpBindingRuntime();
2936
- if (!(await ensureConfiguredAcpBindingReady({
2937
- cfg,
2938
- configuredBinding: _configuredBinding
2939
- })).ok) {
2940
- logInboundDrop({
2941
- log: logVerboseMessage,
2942
- channel: "matrix",
2943
- reason: "configured ACP binding unavailable",
2944
- target: _configuredBinding.spec.conversationId
2945
- });
2946
- discardReservedHistorySlot();
2947
- return;
2948
- }
2949
- }
2950
- if (_runtimeBindingId) {
2951
- const { getSessionBindingService } = await loadSessionBindingRuntime();
2952
- getSessionBindingService().touch(_runtimeBindingId, eventTs ?? void 0);
2953
- }
2954
- const preparedTrigger = isRoom && historyLimit > 0 ? reservedHistorySlot ? roomHistoryTracker.prepareReservedTrigger(_route.agentId, roomId, historyLimit, reservedHistorySlot, {
2955
- sender: senderName,
2956
- body: bodyText,
2957
- timestamp: eventTs ?? void 0,
2958
- messageId
2959
- }, historyThreadId) : roomHistoryTracker.prepareTrigger(_route.agentId, roomId, historyLimit, {
2960
- sender: senderName,
2961
- body: bodyText,
2962
- timestamp: eventTs ?? void 0,
2963
- messageId
2964
- }, historyThreadId) : void 0;
2965
- if (reservedHistorySlot && preparedTrigger) reservedHistorySlotConsumed = true;
2966
- return {
2967
- route: _route,
2968
- hasExplicitSessionBinding,
2969
- roomConfig,
2970
- isDirectMessage,
2971
- isRoom,
2972
- shouldRequireMention,
2973
- wasMentioned,
2974
- effectiveWasMentioned,
2975
- shouldBypassMention,
2976
- canDetectMention,
2977
- commandAuthorized,
2978
- inboundHistory: preparedTrigger ? buildInboundHistoryFromEntries({
2979
- entries: preparedTrigger.history,
2980
- limit: historyLimit
2981
- }) : void 0,
2982
- senderName,
2983
- bodyText,
2984
- commandBodyText,
2985
- media,
2986
- preflightAudioTranscript,
2987
- locationPayload,
2988
- messageId,
2989
- triggerSnapshot: preparedTrigger,
2990
- threadRootId,
2991
- thread,
2992
- botLoopProtection,
2993
- effectiveGroupAllowFrom,
2994
- effectiveRoomUsers
2995
- };
2996
3759
  };
2997
3760
  const ingressResult = historyLimit > 0 ? await runRoomIngress(roomId, async () => {
2998
3761
  const prefix = await readIngressPrefix();
@@ -3015,191 +3778,60 @@ function createMatrixRoomMessageHandler(params) {
3015
3778
  if (!resolvedIngressResult) return;
3016
3779
  if ("deferredPrefix" in resolvedIngressResult) return;
3017
3780
  const { route: _route, hasExplicitSessionBinding, roomConfig, isDirectMessage, isRoom, shouldRequireMention, wasMentioned, effectiveWasMentioned, shouldBypassMention, canDetectMention, commandAuthorized, inboundHistory, senderName, bodyText, commandBodyText, media, preflightAudioTranscript, locationPayload, messageId, triggerSnapshot, threadRootId, thread, botLoopProtection, effectiveGroupAllowFrom, effectiveRoomUsers } = resolvedIngressResult;
3018
- const replyToEventId = resolveMatrixReplyToEventId(event.content);
3019
- const threadTarget = thread.threadId;
3020
- const isRoomContextSenderAllowed = (contextSenderId) => {
3021
- if (!isRoom || !contextSenderId) return true;
3022
- if (effectiveRoomUsers.length > 0) return resolveMatrixAllowListMatch({
3023
- allowList: effectiveRoomUsers,
3024
- userId: contextSenderId
3025
- }).allowed;
3026
- if (groupPolicy === "allowlist" && effectiveGroupAllowFrom.length > 0) return resolveMatrixAllowListMatch({
3027
- allowList: effectiveGroupAllowFrom,
3028
- userId: contextSenderId
3029
- }).allowed;
3030
- return true;
3031
- };
3032
- const shouldIncludeRoomContextSender = (kind, contextSenderId) => evaluateSupplementalContextVisibility({
3033
- mode: contextVisibilityMode,
3034
- kind,
3035
- senderAllowed: isRoomContextSenderAllowed(contextSenderId)
3036
- }).include;
3037
- let threadContext = threadRootId ? await resolveThreadContext({
3038
- roomId,
3039
- threadRootId
3040
- }) : void 0;
3041
- let threadContextBlockedByPolicy = false;
3042
- if (threadContext?.senderId && !shouldIncludeRoomContextSender("thread", threadContext.senderId)) {
3043
- logVerboseMessage(`matrix: drop thread root context (mode=${contextVisibilityMode})`);
3044
- threadContextBlockedByPolicy = true;
3045
- threadContext = void 0;
3046
- }
3047
- let replyContext;
3048
- if (replyToEventId && replyToEventId === threadRootId && threadContext?.summary) replyContext = {
3049
- replyToBody: threadContext.summary,
3050
- replyToSender: threadContext.senderLabel,
3051
- replyToSenderId: threadContext.senderId
3052
- };
3053
- else if (replyToEventId && replyToEventId === threadRootId && threadContextBlockedByPolicy) replyContext = await resolveReplyContext({
3054
- roomId,
3055
- eventId: replyToEventId
3056
- });
3057
- else replyContext = replyToEventId ? await resolveReplyContext({
3058
- roomId,
3059
- eventId: replyToEventId
3060
- }) : void 0;
3061
- const replySenderAllowed = !replyContext?.replyToSenderId || isRoomContextSenderAllowed(replyContext.replyToSenderId);
3062
- const roomName = (isRoom ? await getRoomInfo(roomId) : void 0)?.name;
3063
- const envelopeFrom = isDirectMessage ? senderName : roomName ?? roomId;
3064
- const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
3065
- const storePath = resolveStorePathImpl(cfg.session?.store, { agentId: _route.agentId });
3066
- const buildEnvelope = createChannelInboundEnvelopeBuilderImpl({
3781
+ const inboundContext = await resolveMatrixInboundContext({
3782
+ client,
3783
+ core,
3067
3784
  cfg,
3068
- route: _route
3069
- });
3070
- const sharedDmNoticeSessionKey = threadTarget ? _route.mainSessionKey || _route.sessionKey : _route.sessionKey;
3071
- const sharedDmContextNotice = isDirectMessage ? hasExplicitSessionBinding ? null : resolveMatrixSharedDmContextNotice({
3072
- storePath,
3073
- sessionKey: sharedDmNoticeSessionKey,
3785
+ accountId,
3786
+ runtime,
3787
+ logVerboseMessage,
3074
3788
  roomId,
3075
- accountId: _route.accountId,
3076
- dmSessionScope,
3077
- sentRooms: sharedDmContextNoticeRooms,
3078
- logVerboseMessage
3079
- }) : null;
3080
- const body = buildEnvelope({
3081
- channel: "Matrix",
3082
- from: envelopeFrom,
3083
- timestamp: eventTs ?? void 0,
3084
- body: textWithId
3085
- });
3086
- const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
3087
- const quoteHidden = Boolean(replyContext && !evaluateSupplementalContextVisibility({
3088
- mode: contextVisibilityMode,
3089
- kind: "quote",
3090
- senderAllowed: replySenderAllowed
3091
- }).include);
3092
- const ctxPayload = buildChannelInboundEventContext({
3093
- channel: "matrix",
3094
- contextVisibility: contextVisibilityMode,
3095
- finalize: finalizeInboundContext,
3096
- supplemental: {
3097
- quote: replyContext ? {
3098
- id: threadTarget ? void 0 : replyToEventId ?? void 0,
3099
- body: replyContext.replyToBody,
3100
- sender: replyContext.replyToSender,
3101
- senderAllowed: replySenderAllowed
3102
- } : void 0,
3103
- thread: {
3104
- starterBody: threadContext?.threadStarterBody,
3105
- senderAllowed: threadContext ? true : void 0
3106
- },
3107
- groupSystemPrompt: isRoom ? groupSystemPrompt : void 0
3108
- },
3109
- media: toInboundMediaFacts(media ? [{
3110
- path: media.path,
3111
- url: media.path,
3112
- contentType: media.contentType,
3113
- transcribed: preflightAudioTranscript !== void 0
3114
- }] : void 0),
3789
+ event,
3790
+ eventTs: eventTs ?? void 0,
3791
+ route: _route,
3792
+ isDirectMessage,
3793
+ isRoom,
3794
+ effectiveRoomUsers,
3795
+ groupPolicy,
3796
+ effectiveGroupAllowFrom,
3797
+ contextVisibilityMode,
3798
+ resolveThreadContext,
3799
+ resolveReplyContext,
3800
+ threadRootId,
3801
+ thread,
3802
+ getRoomInfo,
3803
+ senderId,
3804
+ senderName,
3805
+ bodyText,
3806
+ commandBodyText,
3807
+ roomConfig,
3115
3808
  messageId,
3116
- timestamp: eventTs ?? void 0,
3117
- from: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
3118
- sender: {
3119
- id: senderId,
3120
- name: senderName,
3121
- username: senderId.split(":")[0]?.replace(/^@/, "")
3122
- },
3123
- conversation: {
3124
- kind: isDirectMessage ? "direct" : "channel",
3125
- id: roomId,
3126
- label: envelopeFrom,
3127
- nativeChannelId: roomId,
3128
- threadId: threadTarget
3129
- },
3130
- route: {
3131
- agentId: _route.agentId,
3132
- dmScope: _route.dmScope,
3133
- accountId: _route.accountId,
3134
- routeSessionKey: _route.sessionKey
3135
- },
3136
- reply: {
3137
- to: `room:${roomId}`,
3138
- replyToId: threadTarget ? void 0 : replyToEventId ?? void 0,
3139
- messageThreadId: threadTarget,
3140
- nativeChannelId: roomId
3141
- },
3142
- message: {
3143
- body,
3144
- rawBody: bodyText,
3145
- commandBody: commandBodyText,
3146
- bodyForAgent: bodyText,
3147
- inboundHistory: inboundHistory && inboundHistory.length > 0 ? inboundHistory : void 0
3148
- },
3149
- access: {
3150
- ...isRoom ? { mentions: {
3151
- canDetectMention: true,
3152
- wasMentioned,
3153
- requireMention: shouldRequireMention
3154
- } } : {},
3155
- commands: { authorized: commandAuthorized }
3156
- },
3157
- extra: {
3158
- GroupSubject: isRoom ? roomName ?? roomId : void 0,
3159
- GroupId: isRoom ? roomId : void 0,
3160
- GroupChannel: isRoom ? roomId : void 0,
3161
- ...locationPayload?.context,
3162
- CommandSource: "text",
3163
- NativeDirectUserId: isDirectMessage ? senderId : void 0
3164
- }
3165
- });
3166
- if (quoteHidden) logVerboseMessage(`matrix: drop reply context (mode=${contextVisibilityMode})`);
3167
- const preview = truncateUtf16Safe(bodyText, 200).replace(/\n/g, "\\n");
3168
- logVerboseMessage(`matrix inbound: room=${roomId} from=${senderId} preview="${preview}"`);
3169
- const replyTarget = ctxPayload.To;
3170
- if (!replyTarget) {
3171
- runtime.error?.("matrix: missing reply target");
3172
- return;
3173
- }
3174
- const { ackReaction, ackReactionScope: ackScope } = resolveMatrixAckReactionConfig({
3175
- cfg,
3176
- agentId: _route.agentId,
3177
- accountId
3178
- });
3179
- const shouldAckReaction = () => Boolean(ackReaction && core.channel.reactions.shouldAckReaction({
3180
- scope: ackScope,
3181
- isDirect: isDirectMessage,
3182
- isGroup: isRoom,
3183
- isMentionableGroup: isRoom,
3184
- requireMention: shouldRequireMention,
3185
- canDetectMention,
3809
+ inboundHistory,
3810
+ wasMentioned,
3186
3811
  effectiveWasMentioned,
3187
- shouldBypassMention
3188
- }));
3189
- if (shouldAckReaction() && messageId) loadMatrixSendModule().then(({ reactMatrixMessage }) => reactMatrixMessage(roomId, messageId, ackReaction, client)).catch((err) => {
3190
- logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`);
3191
- });
3192
- if (messageId) loadMatrixSendModule().then(({ sendReadReceiptMatrix }) => sendReadReceiptMatrix(roomId, messageId, client)).catch((err) => {
3193
- logVerboseMessage(`matrix: read receipt failed room=${roomId} id=${messageId}: ${String(err)}`);
3812
+ shouldBypassMention,
3813
+ canDetectMention,
3814
+ shouldRequireMention,
3815
+ commandAuthorized,
3816
+ locationPayload,
3817
+ media,
3818
+ preflightAudioTranscript,
3819
+ historyLimit,
3820
+ hasExplicitSessionBinding,
3821
+ dmSessionScope,
3822
+ sharedDmContextNoticeRooms,
3823
+ resolveStorePath: resolveStorePathImpl,
3824
+ createChannelInboundEnvelopeBuilder: createChannelInboundEnvelopeBuilderImpl,
3825
+ finalizeInboundContext
3194
3826
  });
3827
+ if (!inboundContext) return;
3828
+ const { replyToEventId, threadTarget, storePath, ctxPayload, replyTarget, sharedDmContextNotice } = inboundContext;
3195
3829
  const tableMode = core.channel.text.resolveMarkdownTableMode({
3196
3830
  cfg,
3197
3831
  channel: "matrix",
3198
3832
  accountId: _route.accountId
3199
3833
  });
3200
3834
  const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, _route.agentId);
3201
- let finalReplyDeliveryFailed = false;
3202
- let nonFinalReplyDeliveryFailed = false;
3203
3835
  const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
3204
3836
  cfg,
3205
3837
  agentId: _route.agentId,
@@ -3234,431 +3866,47 @@ function createMatrixRoomMessageHandler(params) {
3234
3866
  });
3235
3867
  }
3236
3868
  });
3237
- const draftStreamingEnabled = streaming !== "off";
3238
- const quietDraftStreaming = streaming === "quiet" || streaming === "progress";
3239
- const progressDraftStreaming = streaming === "progress";
3240
- const draftReplyToId = replyToMode !== "off" && !threadTarget ? messageId : void 0;
3241
- const draftStream = draftStreamingEnabled ? await loadMatrixDraftStream().then(({ createMatrixDraftStream }) => createMatrixDraftStream({
3869
+ const draftController = await createMatrixDraftController({
3870
+ streaming,
3871
+ previewToolProgressEnabled,
3872
+ replyToMode,
3873
+ messageId,
3874
+ threadTarget,
3875
+ accountConfig: params.accountConfig,
3876
+ cfg,
3877
+ accountId: _route.accountId,
3242
3878
  roomId,
3243
3879
  client,
3880
+ logVerboseMessage
3881
+ });
3882
+ const { draftStream } = draftController;
3883
+ draftControllerRef = draftController;
3884
+ const replyDispatcher = createMatrixReplyDispatcher({
3244
3885
  cfg,
3245
- mode: quietDraftStreaming ? "quiet" : "partial",
3246
- threadId: threadTarget,
3247
- replyToId: draftReplyToId,
3248
- preserveReplyId: replyToMode === "all",
3249
- accountId: _route.accountId,
3250
- log: logVerboseMessage
3251
- })) : void 0;
3252
- draftStreamRef = draftStream;
3253
- const shouldStreamPreviewToolProgress = Boolean(draftStream) && previewToolProgressEnabled;
3254
- const shouldSuppressDefaultToolProgressMessages = Boolean(draftStream) && (shouldStreamPreviewToolProgress || params.streaming === "progress");
3255
- let currentDraftMessageGeneration = 0;
3256
- let currentDraftBlockOffset = 0;
3257
- let latestDraftFullText = "";
3258
- const pendingDraftBoundaries = [];
3259
- const latestQueuedDraftBoundaryOffsets = /* @__PURE__ */ new Map();
3260
- let currentDraftReplyToId = draftReplyToId;
3261
- let previewToolProgressSuppressed = false;
3262
- let previewToolProgressLines = [];
3263
- let latestPlan;
3264
- let latestPlanExplanation;
3265
- const progressConfigEntry = params.accountConfig ?? cfg.channels?.matrix;
3266
- const progressSeed = `${_route.accountId}:${roomId}`;
3267
- const renderProgressDraft = () => {
3268
- if (!draftStream) return;
3269
- const previewText = formatChannelProgressDraftText({
3270
- entry: progressConfigEntry,
3271
- lines: previewToolProgressLines,
3272
- seed: progressSeed,
3273
- formatLine: formatMatrixToolProgressMarkdownCode,
3274
- bullet: "-",
3275
- narration: latestPlanExplanation,
3276
- plan: latestPlan
3277
- });
3278
- if (!previewText) return;
3279
- draftStream.update(previewText);
3280
- };
3281
- const progressDraftGate = createChannelProgressDraftGate({ onStart: renderProgressDraft });
3282
- const pushPreviewToolProgress = async (line, options) => {
3283
- if (!draftStream) return;
3284
- if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
3285
- const normalized = normalizeChannelProgressDraftLineIdentity(line);
3286
- const progressLine = typeof line === "object" && line !== void 0 ? line : normalized;
3287
- if (!progressDraftStreaming) {
3288
- if (!shouldStreamPreviewToolProgress || previewToolProgressSuppressed || !normalized) return;
3289
- const nextLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(progressConfigEntry) });
3290
- if (nextLines === previewToolProgressLines) return;
3291
- previewToolProgressLines = nextLines;
3292
- draftStream.update(formatChannelProgressDraftText({
3293
- entry: progressConfigEntry,
3294
- lines: previewToolProgressLines,
3295
- seed: progressSeed,
3296
- formatLine: formatMatrixToolProgressMarkdownCode,
3297
- bullet: "-",
3298
- narration: latestPlanExplanation,
3299
- plan: latestPlan
3300
- }));
3301
- return;
3302
- }
3303
- if (shouldStreamPreviewToolProgress && !previewToolProgressSuppressed && normalized) previewToolProgressLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(progressConfigEntry) });
3304
- const alreadyStarted = progressDraftGate.hasStarted;
3305
- const progressActive = await progressDraftGate.noteWork();
3306
- if ((alreadyStarted || progressActive) && progressDraftGate.hasStarted) renderProgressDraft();
3307
- };
3308
- const pushPlanProgress = async (steps, explanation) => {
3309
- latestPlan = steps?.length ? steps.map((entry) => ({ ...entry })) : void 0;
3310
- latestPlanExplanation = explanation?.replace(/\s+/g, " ").trim() || void 0;
3311
- if (!draftStream || previewToolProgressSuppressed) return;
3312
- if (!progressDraftStreaming) {
3313
- renderProgressDraft();
3314
- return;
3315
- }
3316
- const alreadyStarted = progressDraftGate.hasStarted;
3317
- await progressDraftGate.startNow();
3318
- if (alreadyStarted && progressDraftGate.hasStarted) renderProgressDraft();
3319
- };
3320
- const suppressPreviewToolProgressForAnswerText = (text) => {
3321
- if (!text?.trim()) return;
3322
- previewToolProgressSuppressed = true;
3323
- previewToolProgressLines = [];
3324
- latestPlan = void 0;
3325
- latestPlanExplanation = void 0;
3326
- };
3327
- const resetPreviewToolProgress = () => {
3328
- previewToolProgressSuppressed = false;
3329
- previewToolProgressLines = [];
3330
- latestPlan = void 0;
3331
- latestPlanExplanation = void 0;
3332
- };
3333
- const buildPreviewToolProgressReplyOptions = () => {
3334
- if (!shouldSuppressDefaultToolProgressMessages) return {};
3335
- const options = { suppressDefaultToolProgressMessages: true };
3336
- if (!shouldStreamPreviewToolProgress) return options;
3337
- return {
3338
- ...options,
3339
- onToolStart: async (payload) => {
3340
- const toolName = payload.name?.trim();
3341
- await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
3342
- event: "tool",
3343
- itemId: payload.itemId,
3344
- toolCallId: payload.toolCallId,
3345
- name: toolName,
3346
- phase: payload.phase,
3347
- args: payload.args
3348
- }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName });
3349
- },
3350
- onItemEvent: async (payload) => {
3351
- await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
3352
- event: "item",
3353
- itemId: payload.itemId,
3354
- toolCallId: payload.toolCallId,
3355
- itemKind: payload.kind,
3356
- title: payload.title,
3357
- name: payload.name,
3358
- phase: payload.phase,
3359
- status: payload.status,
3360
- summary: payload.summary,
3361
- progressText: payload.progressText,
3362
- meta: payload.meta
3363
- }));
3364
- },
3365
- onPlanUpdate: async (payload) => {
3366
- if (payload.phase !== "update") return;
3367
- await pushPlanProgress(payload.steps, payload.explanation);
3368
- },
3369
- onApprovalEvent: async (payload) => {
3370
- if (payload.phase !== "requested") return;
3371
- await pushPreviewToolProgress(formatChannelProgressDraftLine({
3372
- event: "approval",
3373
- phase: payload.phase,
3374
- title: payload.title,
3375
- command: payload.command,
3376
- reason: payload.reason,
3377
- message: payload.message
3378
- }));
3379
- },
3380
- onCommandOutput: async (payload) => {
3381
- if (payload.phase !== "end") return;
3382
- await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
3383
- event: "command-output",
3384
- itemId: payload.itemId,
3385
- toolCallId: payload.toolCallId,
3386
- phase: payload.phase,
3387
- title: payload.title,
3388
- name: payload.name,
3389
- status: payload.status,
3390
- exitCode: payload.exitCode
3391
- }));
3392
- },
3393
- onPatchSummary: async (payload) => {
3394
- if (payload.phase !== "end") return;
3395
- await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(progressConfigEntry, {
3396
- event: "patch",
3397
- itemId: payload.itemId,
3398
- toolCallId: payload.toolCallId,
3399
- phase: payload.phase,
3400
- title: payload.title,
3401
- name: payload.name,
3402
- added: payload.added,
3403
- modified: payload.modified,
3404
- deleted: payload.deleted,
3405
- summary: payload.summary
3406
- }));
3407
- }
3408
- };
3409
- };
3410
- const getDisplayableDraftText = () => {
3411
- const nextDraftBoundaryOffset = pendingDraftBoundaries.find((boundary) => boundary.messageGeneration === currentDraftMessageGeneration)?.endOffset;
3412
- if (nextDraftBoundaryOffset === void 0) return latestDraftFullText.slice(currentDraftBlockOffset);
3413
- return latestDraftFullText.slice(currentDraftBlockOffset, nextDraftBoundaryOffset);
3414
- };
3415
- const updateDraftFromLatestFullText = () => {
3416
- const blockText = getDisplayableDraftText();
3417
- if (blockText) draftStream?.update(blockText);
3418
- };
3419
- const queueDraftBlockBoundary = (payload, context) => {
3420
- const payloadTextLength = payload.text?.length ?? 0;
3421
- const messageGeneration = context?.assistantMessageIndex ?? currentDraftMessageGeneration;
3422
- const nextDraftBoundaryOffset = (latestQueuedDraftBoundaryOffsets.get(messageGeneration) ?? 0) + payloadTextLength;
3423
- latestQueuedDraftBoundaryOffsets.set(messageGeneration, nextDraftBoundaryOffset);
3424
- pendingDraftBoundaries.push({
3425
- messageGeneration,
3426
- endOffset: nextDraftBoundaryOffset
3427
- });
3428
- };
3429
- const advanceDraftBlockBoundary = (options) => {
3430
- const completedBoundary = pendingDraftBoundaries.shift();
3431
- if (completedBoundary) {
3432
- if (!pendingDraftBoundaries.some((entry) => entry.messageGeneration === completedBoundary.messageGeneration)) latestQueuedDraftBoundaryOffsets.delete(completedBoundary.messageGeneration);
3433
- if (completedBoundary.messageGeneration === currentDraftMessageGeneration) currentDraftBlockOffset = completedBoundary.endOffset;
3434
- return;
3435
- }
3436
- if (options?.fallbackToLatestEnd) currentDraftBlockOffset = latestDraftFullText.length;
3437
- };
3438
- const resetDraftBlockOffsets = () => {
3439
- currentDraftMessageGeneration += 1;
3440
- currentDraftBlockOffset = 0;
3441
- latestDraftFullText = "";
3442
- };
3443
- const resetDraftDeliveryState = async () => {
3444
- await draftStream?.discardPending();
3445
- draftStream?.reset();
3446
- draftConsumed = false;
3447
- currentDraftMessageGeneration = 0;
3448
- currentDraftBlockOffset = 0;
3449
- latestDraftFullText = "";
3450
- pendingDraftBoundaries.length = 0;
3451
- latestQueuedDraftBoundaryOffsets.clear();
3452
- currentDraftReplyToId = draftReplyToId;
3453
- progressDraftGate.reset();
3454
- resetPreviewToolProgress();
3455
- };
3456
- const { deliver: deliverReply, onError: onReplyError, ...turnDispatcherOptions } = {
3457
- ...prefixOptions,
3886
+ prefixOptions,
3458
3887
  humanDelay: resolveHumanDelayConfigImpl(cfg, _route.agentId),
3459
- deliver: async (payload, info) => {
3460
- if (draftStream && info.kind !== "tool" && !payload.isCompactionNotice) {
3461
- const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
3462
- const ttsSupplement = getReplyPayloadTtsSupplement(payload);
3463
- const fallbackPayload = ttsSupplement && ttsSupplement.visibleTextAlreadyDelivered !== true && !payload.text?.trim() ? {
3464
- ...payload,
3465
- text: ttsSupplement.spokenText
3466
- } : payload;
3467
- if (draftConsumed) {
3468
- await draftStream.discardPending();
3469
- await deliverMatrixReplies({
3470
- cfg,
3471
- replies: [fallbackPayload],
3472
- roomId,
3473
- client,
3474
- runtime,
3475
- textLimit,
3476
- replyToMode,
3477
- threadId: threadTarget,
3478
- replyToId: threadTarget ?? replyToEventId ?? void 0,
3479
- accountId: _route.accountId,
3480
- mediaLocalRoots,
3481
- tableMode
3482
- });
3483
- return;
3484
- }
3485
- const payloadReplyToId = normalizeOptionalString(payload.replyToId);
3486
- const payloadReplyMismatch = replyToMode !== "off" && !threadTarget && payloadReplyToId !== currentDraftReplyToId;
3487
- let mustDeliverFinalNormally = draftStream.mustDeliverFinalNormally();
3488
- if (Boolean(payload.text?.trim()) && !payload.isError && !payloadReplyMismatch && !mustDeliverFinalNormally) {
3489
- await draftStream.stop();
3490
- mustDeliverFinalNormally = draftStream.mustDeliverFinalNormally();
3491
- } else await draftStream.discardPending();
3492
- const draftEventId = draftStream.eventId();
3493
- const draftFinalTextNeedsNormalMentionDelivery = Boolean(draftEventId) && typeof payload.text === "string" && Boolean(payload.text.trim()) && !payload.isError && !payloadReplyMismatch && !mustDeliverFinalNormally && await matrixTextWouldActivateMentions(client, payload.text);
3494
- if (draftEventId && payload.text && !payload.isError && !hasMedia && !payloadReplyMismatch && !mustDeliverFinalNormally && !draftFinalTextNeedsNormalMentionDelivery) {
3495
- const finalPreviewText = payload.text;
3496
- await deliverWithFinalizableLivePreviewAdapter({
3497
- kind: "final",
3498
- payload,
3499
- adapter: defineFinalizableLivePreviewAdapter({
3500
- draft: {
3501
- flush: async () => {},
3502
- clear: async () => {},
3503
- discardPending: async () => {},
3504
- id: () => draftEventId
3505
- },
3506
- buildFinalEdit: () => ({
3507
- text: finalPreviewText,
3508
- finalizeLive: !(quietDraftStreaming || !draftStream.matchesPreparedText(finalPreviewText)),
3509
- ...quietDraftStreaming ? { extraContent: buildMatrixFinalizedPreviewContent() } : {}
3510
- }),
3511
- editFinal: async (_draftEventId, edit) => {
3512
- if (edit.finalizeLive) {
3513
- if (!await draftStream.finalizeLive()) throw new Error("Matrix draft live finalize failed");
3514
- return;
3515
- }
3516
- const { editMessageMatrix } = await loadMatrixSendModule();
3517
- await editMessageMatrix(roomId, _draftEventId, edit.text, {
3518
- client,
3519
- cfg,
3520
- threadId: threadTarget,
3521
- accountId: _route.accountId,
3522
- extraContent: edit.extraContent
3523
- });
3524
- },
3525
- createPreviewReceipt: (id) => createPreviewMessageReceipt({
3526
- id,
3527
- ...threadTarget ? { threadId: threadTarget } : {},
3528
- ...currentDraftReplyToId ? { replyToId: currentDraftReplyToId } : {}
3529
- }),
3530
- logPreviewEditFailure: (err) => {
3531
- logVerboseMessage(`matrix: preview final edit failed: ${String(err)}`);
3532
- }
3533
- }),
3534
- deliverNormally: async () => {
3535
- await redactMatrixDraftEvent(client, roomId, draftEventId);
3536
- await deliverMatrixReplies({
3537
- cfg,
3538
- replies: [fallbackPayload],
3539
- roomId,
3540
- client,
3541
- runtime,
3542
- textLimit,
3543
- replyToMode,
3544
- threadId: threadTarget,
3545
- replyToId: threadTarget ?? replyToEventId ?? void 0,
3546
- accountId: _route.accountId,
3547
- mediaLocalRoots,
3548
- tableMode
3549
- });
3550
- }
3551
- });
3552
- draftConsumed = true;
3553
- } else if (draftEventId && hasMedia && !payloadReplyMismatch) {
3554
- let textEditOk = !mustDeliverFinalNormally;
3555
- const payloadText = payload.text ?? ttsSupplement?.spokenText;
3556
- const payloadTextMatchesDraft = typeof payloadText === "string" && draftStream.matchesPreparedText(payloadText);
3557
- const reusesDraftTextUnchanged = typeof payloadText === "string" && Boolean(payloadText.trim()) && payloadTextMatchesDraft;
3558
- const mediaTextNeedsNormalMentionDelivery = typeof payloadText === "string" && Boolean(payloadText.trim()) && await matrixTextWouldActivateMentions(client, payloadText);
3559
- const requiresFinalTextEdit = quietDraftStreaming || typeof payloadText === "string" && !payloadTextMatchesDraft;
3560
- if (textEditOk && mediaTextNeedsNormalMentionDelivery) textEditOk = false;
3561
- else if (textEditOk && payloadText && requiresFinalTextEdit) {
3562
- const { editMessageMatrix } = await loadMatrixSendModule();
3563
- textEditOk = await editMessageMatrix(roomId, draftEventId, payloadText, {
3564
- client,
3565
- cfg,
3566
- threadId: threadTarget,
3567
- accountId: _route.accountId,
3568
- extraContent: quietDraftStreaming ? buildMatrixFinalizedPreviewContent() : void 0
3569
- }).then(() => true, () => false);
3570
- } else if (textEditOk && reusesDraftTextUnchanged) textEditOk = await draftStream.finalizeLive();
3571
- const reusesDraftAsFinalText = Boolean(payloadText?.trim()) && textEditOk;
3572
- if (!reusesDraftAsFinalText) await redactMatrixDraftEvent(client, roomId, draftEventId);
3573
- const mediaPayload = ttsSupplement && reusesDraftAsFinalText ? buildTtsSupplementMediaPayload(payload) : {
3574
- ...payload,
3575
- text: reusesDraftAsFinalText ? void 0 : payload.text ?? (ttsSupplement?.visibleTextAlreadyDelivered === true ? void 0 : ttsSupplement?.spokenText)
3576
- };
3577
- await deliverMatrixReplies({
3578
- cfg,
3579
- replies: [mediaPayload],
3580
- roomId,
3581
- client,
3582
- runtime,
3583
- textLimit,
3584
- replyToMode,
3585
- threadId: threadTarget,
3586
- replyToId: threadTarget ?? replyToEventId ?? void 0,
3587
- accountId: _route.accountId,
3588
- mediaLocalRoots,
3589
- tableMode
3590
- });
3591
- draftConsumed = true;
3592
- } else {
3593
- const draftRedacted = Boolean(draftEventId) && (payload.isError || payloadReplyMismatch || mustDeliverFinalNormally || draftFinalTextNeedsNormalMentionDelivery);
3594
- if (draftRedacted && draftEventId) await redactMatrixDraftEvent(client, roomId, draftEventId);
3595
- const deliveredFallback = await deliverMatrixReplies({
3596
- cfg,
3597
- replies: [fallbackPayload],
3598
- roomId,
3599
- client,
3600
- runtime,
3601
- textLimit,
3602
- replyToMode,
3603
- threadId: threadTarget,
3604
- replyToId: threadTarget ?? replyToEventId ?? void 0,
3605
- accountId: _route.accountId,
3606
- mediaLocalRoots,
3607
- tableMode
3608
- });
3609
- if (draftRedacted || deliveredFallback) draftConsumed = true;
3610
- }
3611
- if (info.kind === "block") {
3612
- draftConsumed = false;
3613
- advanceDraftBlockBoundary({ fallbackToLatestEnd: true });
3614
- draftStream.reset();
3615
- currentDraftReplyToId = replyToMode === "all" ? draftReplyToId : void 0;
3616
- updateDraftFromLatestFullText();
3617
- const { sendTypingMatrix } = await loadMatrixSendModule();
3618
- await sendTypingMatrix(roomId, true, void 0, client).catch(() => {});
3619
- }
3620
- } else await deliverMatrixReplies({
3621
- cfg,
3622
- replies: [payload],
3623
- roomId,
3624
- client,
3625
- runtime,
3626
- textLimit,
3627
- replyToMode,
3628
- threadId: threadTarget,
3629
- replyToId: threadTarget ?? replyToEventId ?? void 0,
3630
- accountId: _route.accountId,
3631
- mediaLocalRoots,
3632
- tableMode
3633
- });
3634
- },
3635
- onError: (err, info) => {
3636
- if (info.kind === "final") finalReplyDeliveryFailed = true;
3637
- else nonFinalReplyDeliveryFailed = true;
3638
- if (info.kind === "block") advanceDraftBlockBoundary({ fallbackToLatestEnd: true });
3639
- runtime.error?.(`matrix ${info.kind} reply failed: ${String(err)}`);
3640
- },
3641
- onReplyStart: typingCallbacks.onReplyStart,
3642
- onIdle: typingCallbacks.onIdle
3643
- };
3888
+ typingCallbacks,
3889
+ streaming,
3890
+ draftStream,
3891
+ draftController,
3892
+ client,
3893
+ roomId,
3894
+ runtime,
3895
+ textLimit,
3896
+ replyToMode,
3897
+ threadTarget,
3898
+ replyToEventId: replyToEventId ?? void 0,
3899
+ accountId: _route.accountId,
3900
+ mediaLocalRoots,
3901
+ tableMode,
3902
+ logVerboseMessage
3903
+ });
3904
+ const { deliverReply, onReplyError, turnDispatcherOptions } = replyDispatcher;
3644
3905
  const pinnedMainDmOwner = isDirectMessage ? await (async () => {
3645
- const livePinnedCfg = core.config.current();
3646
- const livePinnedAllowlists = resolveMatrixAccountAllowlistConfig({
3647
- cfg: livePinnedCfg,
3648
- accountId
3649
- });
3650
- const livePinnedDmAllowFrom = await resolveCachedLiveAllowlist({
3651
- cfg: livePinnedCfg,
3652
- entries: livePinnedAllowlists.dmAllowFrom,
3653
- startupResolvedEntries: allowFromResolvedEntries,
3654
- cache: liveDmAllowlistCache,
3655
- updateCache: (next) => {
3656
- liveDmAllowlistCache = next;
3657
- }
3658
- });
3906
+ const { liveCfg, liveDmAllowFrom } = await handlerState.resolveLiveAccountAllowlists();
3659
3907
  return resolvePinnedMainDmOwnerFromAllowlist({
3660
- dmScope: livePinnedCfg.session?.dmScope,
3661
- allowFrom: livePinnedDmAllowFrom,
3908
+ dmScope: liveCfg.session?.dmScope,
3909
+ allowFrom: liveDmAllowFrom,
3662
3910
  normalizeEntry: normalizeMatrixUserId
3663
3911
  });
3664
3912
  })() : null;
@@ -3726,27 +3974,22 @@ function createMatrixRoomMessageHandler(params) {
3726
3974
  },
3727
3975
  dispatcherOptions: {
3728
3976
  ...turnDispatcherOptions,
3729
- onSettled: () => progressDraftGate.cancel()
3977
+ onSettled: () => draftController.progressDraftGate.cancel()
3730
3978
  },
3731
3979
  replyOptions: {
3732
3980
  skillFilter: roomConfig?.skills,
3733
3981
  disableBlockStreaming: !blockStreamingEnabled,
3734
- onPartialReply: draftStream ? (payload) => {
3735
- if (progressDraftStreaming) return;
3736
- latestDraftFullText = payload.text ?? "";
3737
- suppressPreviewToolProgressForAnswerText(latestDraftFullText);
3738
- updateDraftFromLatestFullText();
3739
- } : void 0,
3982
+ onPartialReply: draftStream ? (payload) => draftController.onPartialReply(payload.text ?? "") : void 0,
3740
3983
  onBlockReplyQueued: draftStream ? (payload, context) => {
3741
3984
  if (payload.isCompactionNotice === true) return;
3742
- queueDraftBlockBoundary(payload, context);
3985
+ draftController.queueDraftBlockBoundary(payload, context);
3743
3986
  } : void 0,
3744
3987
  onAssistantMessageStart: draftStream ? () => {
3745
- resetDraftBlockOffsets();
3746
- resetPreviewToolProgress();
3988
+ draftController.resetDraftBlockOffsets();
3989
+ draftController.resetPreviewToolProgress();
3747
3990
  } : void 0,
3748
- onQueuedFollowupAdmitted: draftStream ? resetDraftDeliveryState : void 0,
3749
- ...buildPreviewToolProgressReplyOptions(),
3991
+ onQueuedFollowupAdmitted: draftStream ? draftController.resetDraftDeliveryState : void 0,
3992
+ ...draftController.buildPreviewToolProgressReplyOptions(),
3750
3993
  onModelSelected
3751
3994
  }
3752
3995
  })
@@ -3758,12 +4001,12 @@ function createMatrixRoomMessageHandler(params) {
3758
4001
  }
3759
4002
  const { dispatchResult } = turnResult;
3760
4003
  const { queuedFinal, counts } = dispatchResult;
3761
- if (finalReplyDeliveryFailed) {
4004
+ if (replyDispatcher.finalReplyDeliveryFailed()) {
3762
4005
  logVerboseMessage(`matrix: final reply delivery failed room=${roomId} id=${messageId}; keeping replay committed`);
3763
4006
  await commitInboundEventIfClaimed();
3764
4007
  return;
3765
4008
  }
3766
- if (!queuedFinal && nonFinalReplyDeliveryFailed) {
4009
+ if (!queuedFinal && replyDispatcher.nonFinalReplyDeliveryFailed()) {
3767
4010
  logVerboseMessage(`matrix: non-final reply delivery failed room=${roomId} id=${messageId}; keeping replay committed`);
3768
4011
  await commitInboundEventIfClaimed();
3769
4012
  return;
@@ -3782,9 +4025,10 @@ function createMatrixRoomMessageHandler(params) {
3782
4025
  } catch (err) {
3783
4026
  runtime.error?.(`matrix handler failed: ${String(err)}`);
3784
4027
  } finally {
3785
- if (draftStreamRef) {
3786
- const draftEventId = await draftStreamRef.stop().catch(() => void 0);
3787
- if (draftEventId && !draftConsumed) await redactMatrixDraftEvent(client, roomId, draftEventId);
4028
+ const draftStream = draftControllerRef?.draftStream;
4029
+ if (draftStream) {
4030
+ const draftEventId = await draftStream.stop().catch(() => void 0);
4031
+ if (draftEventId && draftControllerRef?.isDraftConsumed() !== true) await redactMatrixDraftEvent(client, roomId, draftEventId);
3788
4032
  }
3789
4033
  inboundReplayClaim?.release();
3790
4034
  }
@@ -3805,10 +4049,10 @@ function shouldPromoteRecentInviteRoom(params) {
3805
4049
  //#endregion
3806
4050
  //#region extensions/matrix/src/matrix/monitor/startup.ts
3807
4051
  const loadMatrixStartupMaintenanceDeps = createLazyRuntimeModule(() => Promise.all([
3808
- import("./setup-core-XCdAx1nY.js").then((n) => n.s),
3809
- import("./device-health-D4LBxuPq.js"),
3810
- import("./setup-core-XCdAx1nY.js").then((n) => n.a),
3811
- import("./startup-verification-CA00kebL.js")
4052
+ import("./setup-core-a542Ernq.js").then((n) => n.c),
4053
+ import("./device-health-B4-YXtJR.js").then((n) => n.t),
4054
+ import("./setup-core-a542Ernq.js").then((n) => n.o),
4055
+ import("./startup-verification-wm3_i88s.js")
3812
4056
  ]).then(([configUpdateModule, deviceHealthModule, profileModule, startupVerificationModule]) => ({
3813
4057
  updateMatrixAccountConfig: configUpdateModule.updateMatrixAccountConfig,
3814
4058
  summarizeMatrixDeviceHealth: deviceHealthModule.summarizeMatrixDeviceHealth,
@@ -4037,20 +4281,13 @@ function createMatrixMonitorTaskRunner(params) {
4037
4281
  }
4038
4282
  //#endregion
4039
4283
  //#region extensions/matrix/src/matrix/monitor/index.ts
4040
- function isMatrixStreamingConfig(streaming) {
4041
- return Boolean(streaming && typeof streaming === "object" && !Array.isArray(streaming));
4042
- }
4043
4284
  function resolveMatrixStreamingMode(streaming) {
4044
- if (streaming === true || streaming === "partial") return "partial";
4045
- if (streaming === "quiet") return "quiet";
4046
- if (streaming === "progress") return "progress";
4047
- if (isMatrixStreamingConfig(streaming)) {
4048
- if (streaming.mode === "partial" || streaming.mode === "quiet" || streaming.mode === "progress") return streaming.mode;
4049
- }
4285
+ const mode = streaming?.mode;
4286
+ if (mode === "partial" || mode === "quiet" || mode === "progress") return mode;
4050
4287
  return "off";
4051
4288
  }
4052
4289
  function resolveMatrixPreviewToolProgress(streaming) {
4053
- if (!isMatrixStreamingConfig(streaming)) return true;
4290
+ if (!streaming) return true;
4054
4291
  if (resolveMatrixStreamingMode(streaming) === "progress") return streaming.progress?.toolProgress ?? streaming.preview?.toolProgress ?? true;
4055
4292
  return streaming.preview?.toolProgress ?? true;
4056
4293
  }