@hywkp/sider 0.0.4 → 0.0.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.
@@ -1,34 +1,11 @@
1
1
  import { DEFAULT_ACCOUNT_ID, createReplyPrefixOptions, createTypingCallbacks, formatTextWithAttachmentLinks, normalizeAccountId, resolveOutboundMediaUrls, } from "openclaw/plugin-sdk";
2
+ import { resolveInboundSiderMedia } from "./inbound-media.js";
3
+ import { buildSiderPartsFromReplyPayload } from "./media-upload.js";
2
4
  const CHANNEL_ID = "sider";
3
5
  const DEFAULT_GATEWAY_URL = "http://47.82.167.142:3001";
4
6
  const DEFAULT_CONNECT_TIMEOUT_MS = 8_000;
5
7
  const DEFAULT_SEND_TIMEOUT_MS = 12_000;
6
8
  const DEFAULT_RECONNECT_DELAY_MS = 2_000;
7
- const IMAGE_EXTENSIONS = new Set([
8
- ".jpg",
9
- ".jpeg",
10
- ".png",
11
- ".gif",
12
- ".webp",
13
- ".bmp",
14
- ".svg",
15
- ".heic",
16
- ".heif",
17
- ".avif",
18
- ]);
19
- const MIME_BY_EXTENSION = {
20
- ".jpg": "image/jpeg",
21
- ".jpeg": "image/jpeg",
22
- ".png": "image/png",
23
- ".gif": "image/gif",
24
- ".webp": "image/webp",
25
- ".bmp": "image/bmp",
26
- ".svg": "image/svg+xml",
27
- ".pdf": "application/pdf",
28
- ".txt": "text/plain",
29
- ".json": "application/json",
30
- ".zip": "application/zip",
31
- };
32
9
  const meta = {
33
10
  id: CHANNEL_ID,
34
11
  label: "Sider",
@@ -40,6 +17,9 @@ const meta = {
40
17
  order: 97,
41
18
  };
42
19
  let runtimeRef = null;
20
+ const SIDER_SESSION_BINDING_TTL_MS = 6 * 60 * 60 * 1000;
21
+ const SIDER_SESSION_BINDING_MAX = 512;
22
+ const siderSessionBindings = new Map();
43
23
  export function setSiderRuntime(runtime) {
44
24
  runtimeRef = runtime;
45
25
  }
@@ -65,6 +45,96 @@ function logWarn(message, data) {
65
45
  function sleep(ms) {
66
46
  return new Promise((resolve) => setTimeout(resolve, ms));
67
47
  }
48
+ function normalizeSessionBindingKey(raw) {
49
+ const key = raw?.trim().toLowerCase();
50
+ return key || undefined;
51
+ }
52
+ function pruneSiderSessionBindings(now = Date.now()) {
53
+ for (const [key, binding] of siderSessionBindings) {
54
+ if (now - binding.lastSeenAt > SIDER_SESSION_BINDING_TTL_MS) {
55
+ siderSessionBindings.delete(key);
56
+ }
57
+ }
58
+ if (siderSessionBindings.size <= SIDER_SESSION_BINDING_MAX) {
59
+ return;
60
+ }
61
+ const sorted = [...siderSessionBindings.entries()].sort((a, b) => a[1].lastSeenAt - b[1].lastSeenAt);
62
+ const overflow = siderSessionBindings.size - SIDER_SESSION_BINDING_MAX;
63
+ for (let index = 0; index < overflow; index += 1) {
64
+ const key = sorted[index]?.[0];
65
+ if (key) {
66
+ siderSessionBindings.delete(key);
67
+ }
68
+ }
69
+ }
70
+ function rememberSiderSessionBinding(params) {
71
+ const key = normalizeSessionBindingKey(params.sessionKey);
72
+ if (!key) {
73
+ return;
74
+ }
75
+ const now = Date.now();
76
+ const existing = siderSessionBindings.get(key);
77
+ if (existing) {
78
+ existing.account = params.account;
79
+ existing.sessionId = params.sessionId;
80
+ existing.lastSeenAt = now;
81
+ pruneSiderSessionBindings(now);
82
+ return;
83
+ }
84
+ siderSessionBindings.set(key, {
85
+ account: params.account,
86
+ sessionId: params.sessionId,
87
+ lastSeenAt: now,
88
+ toolSeq: 0,
89
+ currentToolCallId: undefined,
90
+ callIdByToolCallId: new Map(),
91
+ });
92
+ pruneSiderSessionBindings(now);
93
+ }
94
+ function resolveSiderSessionBinding(sessionKey) {
95
+ const key = normalizeSessionBindingKey(sessionKey);
96
+ if (!key) {
97
+ return undefined;
98
+ }
99
+ const binding = siderSessionBindings.get(key);
100
+ if (!binding) {
101
+ return undefined;
102
+ }
103
+ binding.lastSeenAt = Date.now();
104
+ return binding;
105
+ }
106
+ function toJsonSafeValue(value) {
107
+ if (value === undefined) {
108
+ return undefined;
109
+ }
110
+ try {
111
+ return JSON.parse(JSON.stringify(value));
112
+ }
113
+ catch {
114
+ return String(value);
115
+ }
116
+ }
117
+ function extractToolResultText(result) {
118
+ const maxChars = 4000;
119
+ const clip = (text) => {
120
+ if (text.length <= maxChars) {
121
+ return text;
122
+ }
123
+ return `${text.slice(0, maxChars)}...`;
124
+ };
125
+ const record = toRecord(result);
126
+ if (!record) {
127
+ return "";
128
+ }
129
+ const candidateKeys = ["text", "content", "message", "output", "stdout"];
130
+ for (const key of candidateKeys) {
131
+ const value = record[key];
132
+ if (typeof value === "string" && value.trim()) {
133
+ return clip(value.trim());
134
+ }
135
+ }
136
+ return "";
137
+ }
68
138
  function toRecord(value) {
69
139
  if (!value || typeof value !== "object" || Array.isArray(value)) {
70
140
  return null;
@@ -138,6 +208,26 @@ function parseSessionTarget(raw) {
138
208
  }
139
209
  return trimmed;
140
210
  }
211
+ function normalizeSiderMessagingTarget(raw) {
212
+ const trimmed = raw.trim();
213
+ if (!trimmed) {
214
+ return undefined;
215
+ }
216
+ const withoutProviderPrefix = trimmed.replace(/^sider:/i, "").trim();
217
+ return withoutProviderPrefix || undefined;
218
+ }
219
+ function looksLikeSiderTargetId(raw, normalized) {
220
+ const candidate = (normalized ?? raw ?? "").trim();
221
+ if (!candidate) {
222
+ return false;
223
+ }
224
+ const match = candidate.match(/^session:(.+)$/i);
225
+ if (match) {
226
+ return match[1].trim().length > 0;
227
+ }
228
+ // Sider has no directory lookup yet; treat non-whitespace tokens as explicit ids.
229
+ return !/\s/.test(candidate);
230
+ }
141
231
  function resolveOutboundSessionId(params) {
142
232
  const target = params.to?.trim() || params.account.defaultTo?.trim() || "";
143
233
  if (!target) {
@@ -415,94 +505,6 @@ async function sendSiderEventBestEffort(params) {
415
505
  });
416
506
  }
417
507
  }
418
- function extFromMediaUrl(raw) {
419
- try {
420
- if (/^https?:\/\//i.test(raw)) {
421
- const pathname = new URL(raw).pathname;
422
- return pathname.includes(".") ? pathname.slice(pathname.lastIndexOf(".")).toLowerCase() : "";
423
- }
424
- }
425
- catch {
426
- // no-op
427
- }
428
- const qPos = raw.indexOf("?");
429
- const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
430
- const slashPos = sanitized.lastIndexOf("/");
431
- const last = slashPos >= 0 ? sanitized.slice(slashPos + 1) : sanitized;
432
- const dotPos = last.lastIndexOf(".");
433
- return dotPos >= 0 ? last.slice(dotPos).toLowerCase() : "";
434
- }
435
- function fileNameFromMediaUrl(raw) {
436
- try {
437
- if (/^https?:\/\//i.test(raw)) {
438
- const pathname = new URL(raw).pathname;
439
- const seg = pathname.split("/").filter(Boolean).pop();
440
- return seg || undefined;
441
- }
442
- }
443
- catch {
444
- // no-op
445
- }
446
- const qPos = raw.indexOf("?");
447
- const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
448
- const seg = sanitized.split("/").filter(Boolean).pop();
449
- return seg || undefined;
450
- }
451
- function inferMediaKind(mediaUrl) {
452
- const ext = extFromMediaUrl(mediaUrl);
453
- return IMAGE_EXTENSIONS.has(ext) ? "image" : "file";
454
- }
455
- function inferMediaMimeType(mediaUrl) {
456
- const ext = extFromMediaUrl(mediaUrl);
457
- return MIME_BY_EXTENSION[ext];
458
- }
459
- async function uploadMediaToSider(params) {
460
- const mediaKind = inferMediaKind(params.mediaUrl);
461
- const fileName = fileNameFromMediaUrl(params.mediaUrl);
462
- const mimeType = inferMediaMimeType(params.mediaUrl);
463
- // TODO(siderclaw-upload): Upload local/remote media to sider server and return hosted resource URL.
464
- // Current placeholder returns the original mediaUrl directly.
465
- const resourceUrl = params.mediaUrl;
466
- void params.account;
467
- return {
468
- resourceUrl,
469
- mediaKind,
470
- mimeType,
471
- fileName,
472
- };
473
- }
474
- async function buildSiderPartsFromReplyPayload(params) {
475
- const parts = [];
476
- const text = params.payload.text?.trim();
477
- if (text) {
478
- parts.push({
479
- type: "core.text",
480
- spec_version: 1,
481
- payload: { text },
482
- });
483
- }
484
- const mediaUrls = resolveOutboundMediaUrls(params.payload);
485
- for (const mediaUrl of mediaUrls) {
486
- const uploaded = await uploadMediaToSider({
487
- account: params.account,
488
- mediaUrl,
489
- });
490
- parts.push({
491
- type: "core.media",
492
- spec_version: 1,
493
- payload: {
494
- media_type: uploaded.mediaKind,
495
- url: uploaded.resourceUrl,
496
- mime_type: uploaded.mimeType,
497
- file_name: uploaded.fileName,
498
- },
499
- meta: {
500
- source_media_url: mediaUrl,
501
- },
502
- });
503
- }
504
- return parts;
505
- }
506
508
  function parseTextFromPart(part) {
507
509
  if (part.type !== "core.text") {
508
510
  return null;
@@ -519,21 +521,27 @@ function parseTextFromPart(part) {
519
521
  return null;
520
522
  }
521
523
  function parseMediaFromPart(part) {
522
- if (part.type !== "core.media") {
524
+ if (part.type !== "core.media" && part.type !== "core.file") {
523
525
  return null;
524
526
  }
525
527
  const payload = toRecord(part.payload);
526
528
  if (!payload) {
527
529
  return null;
528
530
  }
529
- const urlCandidates = [payload.url, payload.resource_url, payload.media_url];
531
+ const urlCandidates = [payload.download_url, payload.url, payload.resource_url, payload.media_url];
530
532
  const url = urlCandidates.find((entry) => typeof entry === "string" && entry.trim());
531
533
  if (!url) {
532
534
  return null;
533
535
  }
534
- const mimeCandidates = [payload.mime_type, payload.content_type];
536
+ const mimeCandidates = [payload.mime, payload.mime_type, payload.content_type];
535
537
  const mimeType = mimeCandidates.find((entry) => typeof entry === "string" && entry.trim());
536
- return { url: url.trim(), mimeType: mimeType?.trim() || undefined };
538
+ const fileNameCandidates = [payload.name, payload.file_name];
539
+ const fileName = fileNameCandidates.find((entry) => typeof entry === "string" && entry.trim());
540
+ return {
541
+ url: url.trim(),
542
+ mimeType: mimeType?.trim() || undefined,
543
+ fileName: fileName?.trim() || undefined,
544
+ };
537
545
  }
538
546
  function buildEventMeta(params) {
539
547
  return {
@@ -594,6 +602,147 @@ function buildStreamingDoneEvent(params) {
594
602
  meta: buildEventMeta({ accountId: params.accountId }),
595
603
  };
596
604
  }
605
+ function buildToolCallEvent(params) {
606
+ return {
607
+ eventType: "tool.call",
608
+ payload: {
609
+ session_id: params.sessionId,
610
+ seq: params.seq,
611
+ call_id: params.callId,
612
+ phase: params.phase,
613
+ tool_name: params.toolName,
614
+ tool_call_id: params.toolCallId,
615
+ run_id: params.runId,
616
+ session_key: params.sessionKey,
617
+ tool_args: params.toolArgs,
618
+ error: params.error,
619
+ duration_ms: params.durationMs,
620
+ ts: Date.now(),
621
+ },
622
+ meta: buildEventMeta({ accountId: params.accountId }),
623
+ };
624
+ }
625
+ function buildToolResultEvent(params) {
626
+ const text = extractToolResultText(params.result);
627
+ const safeResult = toJsonSafeValue(params.result);
628
+ const safeToolArgs = toJsonSafeValue(params.toolArgs);
629
+ return {
630
+ eventType: "tool.result",
631
+ payload: {
632
+ session_id: params.sessionId,
633
+ seq: params.seq,
634
+ call_id: params.callId,
635
+ tool_name: params.toolName,
636
+ tool_call_id: params.toolCallId,
637
+ run_id: params.runId,
638
+ session_key: params.sessionKey,
639
+ tool_args: safeToolArgs,
640
+ result: safeResult,
641
+ error: params.error,
642
+ duration_ms: params.durationMs,
643
+ text,
644
+ has_text: text.trim().length > 0,
645
+ media_urls: [],
646
+ media_count: 0,
647
+ is_error: Boolean(params.error),
648
+ ts: Date.now(),
649
+ },
650
+ meta: buildEventMeta({ accountId: params.accountId }),
651
+ };
652
+ }
653
+ function resolveRelayCallIdForToolEvent(params) {
654
+ if (params.toolCallId) {
655
+ const existing = params.binding.callIdByToolCallId.get(params.toolCallId);
656
+ if (existing) {
657
+ params.binding.currentToolCallId = existing;
658
+ return existing;
659
+ }
660
+ const created = crypto.randomUUID();
661
+ params.binding.callIdByToolCallId.set(params.toolCallId, created);
662
+ params.binding.currentToolCallId = created;
663
+ return created;
664
+ }
665
+ if (params.phase === "start" || !params.binding.currentToolCallId) {
666
+ params.binding.currentToolCallId = crypto.randomUUID();
667
+ }
668
+ return params.binding.currentToolCallId;
669
+ }
670
+ function clearRelayCallIdForToolEvent(params) {
671
+ if (params.toolCallId) {
672
+ params.binding.callIdByToolCallId.delete(params.toolCallId);
673
+ }
674
+ if (params.binding.currentToolCallId === params.callId) {
675
+ params.binding.currentToolCallId = undefined;
676
+ }
677
+ }
678
+ export async function emitSiderToolHookEvent(params) {
679
+ const binding = resolveSiderSessionBinding(params.sessionKey);
680
+ if (!binding) {
681
+ logDebug("skip sider tool hook event: session binding not found", {
682
+ sessionKey: params.sessionKey,
683
+ toolName: params.toolName,
684
+ toolCallId: params.toolCallId,
685
+ phase: params.phase,
686
+ });
687
+ return;
688
+ }
689
+ const callId = resolveRelayCallIdForToolEvent({
690
+ binding,
691
+ phase: params.phase,
692
+ toolCallId: params.toolCallId,
693
+ });
694
+ if (params.phase === "start") {
695
+ binding.toolSeq += 1;
696
+ const toolCallEvent = buildToolCallEvent({
697
+ sessionId: binding.sessionId,
698
+ accountId: binding.account.accountId,
699
+ seq: binding.toolSeq,
700
+ callId,
701
+ phase: "start",
702
+ toolName: params.toolName,
703
+ toolCallId: params.toolCallId,
704
+ runId: params.runId,
705
+ sessionKey: params.sessionKey,
706
+ toolArgs: params.params,
707
+ error: params.error,
708
+ durationMs: params.durationMs,
709
+ });
710
+ await sendSiderEventBestEffort({
711
+ account: binding.account,
712
+ sessionId: binding.sessionId,
713
+ event: toolCallEvent,
714
+ context: "tool.call.hook.start",
715
+ });
716
+ }
717
+ if (params.phase !== "start") {
718
+ binding.toolSeq += 1;
719
+ const toolResultEvent = buildToolResultEvent({
720
+ sessionId: binding.sessionId,
721
+ accountId: binding.account.accountId,
722
+ seq: binding.toolSeq,
723
+ callId,
724
+ toolName: params.toolName,
725
+ toolCallId: params.toolCallId,
726
+ runId: params.runId,
727
+ sessionKey: params.sessionKey,
728
+ toolArgs: params.params,
729
+ result: params.result,
730
+ error: params.error,
731
+ durationMs: params.durationMs,
732
+ });
733
+ await sendSiderEventBestEffort({
734
+ account: binding.account,
735
+ sessionId: binding.sessionId,
736
+ event: toolResultEvent,
737
+ context: `tool.result.hook.${params.phase}`,
738
+ });
739
+ clearRelayCallIdForToolEvent({
740
+ binding,
741
+ callId,
742
+ toolCallId: params.toolCallId,
743
+ });
744
+ }
745
+ }
597
746
  async function openStreamingSessionIfNeeded(params) {
598
747
  if (params.streamState.active && params.streamState.streamId) {
599
748
  return;
@@ -752,34 +901,42 @@ async function handleStreamingPartialSnapshot(params) {
752
901
  params.streamState.accumulatedBlockText = params.snapshot;
753
902
  }
754
903
  async function deliverReplyPayloadToSider(params) {
904
+ const payloadMediaUrls = resolveOutboundMediaUrls(params.payload);
755
905
  if (params.kind === "block") {
756
906
  const delta = typeof params.payload.text === "string" ? params.payload.text : "";
757
- if (delta.length === 0) {
758
- return;
759
- }
760
- params.streamState.blockDeltaCount += 1;
761
- mergeBlockTextIntoStreamState({
762
- streamState: params.streamState,
763
- text: delta,
764
- });
765
- if (params.streamState.partialDeltaCount === 0) {
766
- await sendStreamingDeltaEvent({
767
- account: params.account,
768
- sessionId: params.sessionId,
907
+ if (delta.length > 0) {
908
+ params.streamState.blockDeltaCount += 1;
909
+ mergeBlockTextIntoStreamState({
769
910
  streamState: params.streamState,
770
- delta,
771
- context: "stream.delta.block",
911
+ text: delta,
772
912
  });
913
+ if (params.streamState.partialDeltaCount === 0) {
914
+ await sendStreamingDeltaEvent({
915
+ account: params.account,
916
+ sessionId: params.sessionId,
917
+ streamState: params.streamState,
918
+ delta,
919
+ context: "stream.delta.block",
920
+ });
921
+ }
922
+ else {
923
+ logDebug("skip block stream delta because partial streaming is active", {
924
+ accountId: params.account.accountId,
925
+ sessionId: params.sessionId,
926
+ deltaLength: delta.length,
927
+ partialDeltaCount: params.streamState.partialDeltaCount,
928
+ });
929
+ }
773
930
  }
774
- else {
775
- logDebug("skip block stream delta because partial streaming is active", {
776
- accountId: params.account.accountId,
777
- sessionId: params.sessionId,
778
- deltaLength: delta.length,
779
- partialDeltaCount: params.streamState.partialDeltaCount,
780
- });
931
+ if (payloadMediaUrls.length === 0) {
932
+ return;
781
933
  }
782
- return;
934
+ logDebug("block payload contains media; sending persisted sider message", {
935
+ accountId: params.account.accountId,
936
+ sessionId: params.sessionId,
937
+ mediaCount: payloadMediaUrls.length,
938
+ hasText: delta.length > 0,
939
+ });
783
940
  }
784
941
  if (params.kind === "final") {
785
942
  await flushStreamEventQueue(params.streamState);
@@ -810,20 +967,39 @@ async function deliverReplyPayloadToSider(params) {
810
967
  }
811
968
  const parts = await buildSiderPartsFromReplyPayload({
812
969
  account: params.account,
970
+ sessionId: params.sessionId,
813
971
  payload: params.payload,
972
+ logger: {
973
+ debug: logDebug,
974
+ warn: logWarn,
975
+ },
814
976
  });
815
977
  if (parts.length === 0) {
978
+ logDebug("skip sider outbound message: empty parts", {
979
+ accountId: params.account.accountId,
980
+ sessionId: params.sessionId,
981
+ kind: params.kind,
982
+ hasText: typeof params.payload.text === "string" && params.payload.text.length > 0,
983
+ mediaCount: payloadMediaUrls.length,
984
+ });
816
985
  return;
817
986
  }
987
+ logDebug("sending sider outbound message from payload", {
988
+ accountId: params.account.accountId,
989
+ sessionId: params.sessionId,
990
+ kind: params.kind,
991
+ partTypes: parts.map((part) => part.type),
992
+ mediaCount: payloadMediaUrls.length,
993
+ });
818
994
  await sendMessageToSider({
819
995
  account: params.account,
820
996
  sessionId: params.sessionId,
821
997
  parts,
822
998
  });
823
- if (params.kind === "final" &&
824
- typeof params.payload.text === "string" &&
825
- params.payload.text.trim()) {
999
+ if (params.kind === "final" || payloadMediaUrls.length > 0) {
826
1000
  params.streamState.persistedFinalText = true;
1001
+ }
1002
+ if (params.kind === "final") {
827
1003
  params.streamState.accumulatedBlockText = "";
828
1004
  params.streamState.blockDeltaCount = 0;
829
1005
  params.streamState.partialDeltaCount = 0;
@@ -856,7 +1032,7 @@ async function handleInboundRealtimeMessage(params) {
856
1032
  }
857
1033
  const rawText = textChunks.join("\n").trim();
858
1034
  const mediaUrls = mediaItems.map((item) => item.url);
859
- const mediaTypes = mediaItems
1035
+ const parsedMediaTypes = mediaItems
860
1036
  .map((item) => item.mimeType)
861
1037
  .filter((item) => Boolean(item));
862
1038
  const hasControlCommand = rawText ? core.channel.text.hasControlCommand(rawText, cfg) : false;
@@ -895,7 +1071,27 @@ async function handleInboundRealtimeMessage(params) {
895
1071
  commandAuthorized,
896
1072
  });
897
1073
  }
898
- const bodyForAgent = formatTextWithAttachmentLinks(rawText, mediaUrls);
1074
+ const resolvedInboundMedia = await resolveInboundSiderMedia({
1075
+ runtime: core,
1076
+ cfg,
1077
+ accountId: account.accountId,
1078
+ mediaItems,
1079
+ logger: {
1080
+ debug: logDebug,
1081
+ warn: logWarn,
1082
+ },
1083
+ });
1084
+ const unresolvedMediaUrls = resolvedInboundMedia.unresolvedMedia.map((item) => item.url);
1085
+ if (unresolvedMediaUrls.length > 0) {
1086
+ logWarn("sider inbound media fallback to attachment url", {
1087
+ accountId: account.accountId,
1088
+ sessionId,
1089
+ unresolvedCount: unresolvedMediaUrls.length,
1090
+ unresolvedMediaUrls,
1091
+ });
1092
+ }
1093
+ const bodyForAgent = formatTextWithAttachmentLinks(rawText, unresolvedMediaUrls);
1094
+ const mediaPayload = resolvedInboundMedia.mediaPayload;
899
1095
  const route = core.channel.routing.resolveAgentRoute({
900
1096
  cfg,
901
1097
  channel: CHANNEL_ID,
@@ -937,12 +1133,21 @@ async function handleInboundRealtimeMessage(params) {
937
1133
  WasMentioned: true,
938
1134
  OriginatingChannel: CHANNEL_ID,
939
1135
  OriginatingTo: to,
940
- MediaUrl: mediaUrls[0],
941
- MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
942
- MediaType: mediaTypes[0],
943
- MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
1136
+ ...mediaPayload,
1137
+ MediaUrl: mediaPayload.MediaUrl ||
1138
+ (unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls[0] : undefined),
1139
+ MediaUrls: mediaPayload.MediaUrls ||
1140
+ (unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls : undefined),
1141
+ MediaType: mediaPayload.MediaType || parsedMediaTypes[0],
1142
+ MediaTypes: mediaPayload.MediaTypes ||
1143
+ (parsedMediaTypes.length > 0 ? parsedMediaTypes : undefined),
944
1144
  CommandAuthorized: commandAuthorized,
945
1145
  });
1146
+ rememberSiderSessionBinding({
1147
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
1148
+ account,
1149
+ sessionId,
1150
+ });
946
1151
  const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
947
1152
  agentId: route.agentId,
948
1153
  });
@@ -971,6 +1176,8 @@ async function handleInboundRealtimeMessage(params) {
971
1176
  commandAuthorized,
972
1177
  textLength: rawText.length,
973
1178
  mediaCount: mediaUrls.length,
1179
+ mediaDownloadedCount: mediaPayload.MediaPaths?.length ?? 0,
1180
+ mediaDownloadFailedCount: unresolvedMediaUrls.length,
974
1181
  });
975
1182
  const streamState = {
976
1183
  active: false,
@@ -1128,8 +1335,6 @@ async function handleInboundRealtimeMessage(params) {
1128
1335
  });
1129
1336
  streamState.persistedFinalText = true;
1130
1337
  streamState.accumulatedBlockText = "";
1131
- streamState.partialDeltaCount = 0;
1132
- streamState.partialSnapshot = "";
1133
1338
  }
1134
1339
  }
1135
1340
  async function handleInboundRealtimeEvent(params) {
@@ -1360,7 +1565,7 @@ export const siderPlugin = {
1360
1565
  conversationId: result.conversationId,
1361
1566
  };
1362
1567
  },
1363
- sendMedia: async ({ cfg, accountId, to, text, mediaUrl }) => {
1568
+ sendMedia: async ({ cfg, accountId, to, text, mediaUrl, mediaLocalRoots }) => {
1364
1569
  const account = resolveSiderAccount(cfg, accountId);
1365
1570
  if (!account.configured) {
1366
1571
  throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
@@ -1368,10 +1573,16 @@ export const siderPlugin = {
1368
1573
  const sessionId = resolveOutboundSessionId({ account, to });
1369
1574
  const parts = await buildSiderPartsFromReplyPayload({
1370
1575
  account,
1576
+ sessionId,
1371
1577
  payload: {
1372
1578
  text,
1373
1579
  mediaUrl,
1374
1580
  },
1581
+ mediaLocalRoots,
1582
+ logger: {
1583
+ debug: logDebug,
1584
+ warn: logWarn,
1585
+ },
1375
1586
  });
1376
1587
  if (parts.length === 0) {
1377
1588
  throw new Error("sider sendMedia requires text and/or mediaUrl");
@@ -1404,5 +1615,12 @@ export const siderPlugin = {
1404
1615
  ctx.log?.info(`[${account.accountId}] sider relay monitor stopped`);
1405
1616
  },
1406
1617
  },
1618
+ messaging: {
1619
+ normalizeTarget: normalizeSiderMessagingTarget,
1620
+ targetResolver: {
1621
+ looksLikeId: looksLikeSiderTargetId,
1622
+ hint: "session:<sessionId> (or raw <sessionId>)",
1623
+ },
1624
+ },
1407
1625
  };
1408
1626
  //# sourceMappingURL=channel.js.map