@sider-ai/chrome-openclaw-sider 1.0.31

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 (61) hide show
  1. package/README.md +75 -0
  2. package/README.zh_CN.md +108 -0
  3. package/dist/account-cW9SLuNu.d.ts +75 -0
  4. package/dist/index.d.ts +12 -0
  5. package/dist/index.js +87 -0
  6. package/dist/setup-entry.d.ts +6 -0
  7. package/dist/setup-entry.js +11 -0
  8. package/dist/setup-plugin-api.d.ts +7 -0
  9. package/dist/setup-plugin-api.js +4 -0
  10. package/dist/src/account.d.ts +6 -0
  11. package/dist/src/account.js +260 -0
  12. package/dist/src/auth.d.ts +30 -0
  13. package/dist/src/auth.js +225 -0
  14. package/dist/src/channel-auto-title.d.ts +6 -0
  15. package/dist/src/channel-auto-title.js +102 -0
  16. package/dist/src/channel-builders.d.ts +105 -0
  17. package/dist/src/channel-builders.js +238 -0
  18. package/dist/src/channel-hooks.d.ts +30 -0
  19. package/dist/src/channel-hooks.js +380 -0
  20. package/dist/src/channel-monitor.d.ts +34 -0
  21. package/dist/src/channel-monitor.js +335 -0
  22. package/dist/src/channel-parts.d.ts +26 -0
  23. package/dist/src/channel-parts.js +32 -0
  24. package/dist/src/channel-relay.d.ts +117 -0
  25. package/dist/src/channel-relay.js +574 -0
  26. package/dist/src/channel-runtime.d.ts +33 -0
  27. package/dist/src/channel-runtime.js +138 -0
  28. package/dist/src/channel-send-result.d.ts +19 -0
  29. package/dist/src/channel-send-result.js +126 -0
  30. package/dist/src/channel-send.d.ts +73 -0
  31. package/dist/src/channel-send.js +291 -0
  32. package/dist/src/channel-session-model.d.ts +19 -0
  33. package/dist/src/channel-session-model.js +244 -0
  34. package/dist/src/channel-shared.d.ts +153 -0
  35. package/dist/src/channel-shared.js +96 -0
  36. package/dist/src/channel-state.d.ts +93 -0
  37. package/dist/src/channel-state.js +475 -0
  38. package/dist/src/channel-streaming.d.ts +117 -0
  39. package/dist/src/channel-streaming.js +681 -0
  40. package/dist/src/channel-types.d.ts +209 -0
  41. package/dist/src/channel-types.js +40 -0
  42. package/dist/src/channel-typing.d.ts +17 -0
  43. package/dist/src/channel-typing.js +79 -0
  44. package/dist/src/channel-util.d.ts +78 -0
  45. package/dist/src/channel-util.js +604 -0
  46. package/dist/src/channel.d.ts +14 -0
  47. package/dist/src/channel.js +834 -0
  48. package/dist/src/channel.setup.d.ts +10 -0
  49. package/dist/src/channel.setup.js +9 -0
  50. package/dist/src/config.d.ts +18 -0
  51. package/dist/src/config.js +38 -0
  52. package/dist/src/inbound-media.d.ts +37 -0
  53. package/dist/src/inbound-media.js +148 -0
  54. package/dist/src/media-upload.d.ts +87 -0
  55. package/dist/src/media-upload.js +1308 -0
  56. package/dist/src/setup-core.d.ts +72 -0
  57. package/dist/src/setup-core.js +343 -0
  58. package/dist/src/user-agent.d.ts +4 -0
  59. package/dist/src/user-agent.js +6 -0
  60. package/openclaw.plugin.json +96 -0
  61. package/package.json +47 -0
@@ -0,0 +1,834 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { jsonResult, readStringParam } from "openclaw/plugin-sdk/agent-runtime";
3
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
4
+ import {
5
+ loadConfig,
6
+ writeConfigFile
7
+ } from "openclaw/plugin-sdk/config-runtime";
8
+ import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
9
+ import {
10
+ formatTextWithAttachmentLinks,
11
+ resolveOutboundMediaUrls
12
+ } from "openclaw/plugin-sdk/reply-payload";
13
+ import {
14
+ describeSiderAccountConfigurationError,
15
+ extractSiderToolSend,
16
+ isSiderAccountBootstrappable,
17
+ listSiderAccountIds,
18
+ looksLikeSiderTargetId,
19
+ normalizeSiderMessagingTarget,
20
+ resolveManagedSiderAccount,
21
+ resolveOutboundSessionId,
22
+ resolveSiderAccount
23
+ } from "./account.js";
24
+ import {
25
+ isFirstTurnInSiderSession,
26
+ resolveAutoTitleConfig,
27
+ scheduleSiderAutoTitleUpdate
28
+ } from "./channel-auto-title.js";
29
+ import {
30
+ maybeResetSiderSessionToCurrentDefaultModel
31
+ } from "./channel-session-model.js";
32
+ import {
33
+ SIDER_CHANNEL_ID,
34
+ SIDER_PUSH_TOOL_ERROR_WARNINGS,
35
+ SIDER_TOOL_ERROR_WARNING_TEXT_RE
36
+ } from "./config.js";
37
+ import { resolveInboundSiderMedia } from "./inbound-media.js";
38
+ import {
39
+ buildOptimisticSiderPartFromInlineAttachment
40
+ } from "./media-upload.js";
41
+ import {
42
+ applySiderSetupAccountConfig,
43
+ createSiderPairingPendingUpdateReporter,
44
+ formatSiderPairingInstructions,
45
+ requestSiderPairing,
46
+ SiderPairingExpiredError,
47
+ waitForSiderPairing
48
+ } from "./setup-core.js";
49
+ import { siderChannelPluginCommon } from "./channel-shared.js";
50
+ import {
51
+ getSiderRuntime,
52
+ logDebug,
53
+ logError,
54
+ logInfo,
55
+ logWarn,
56
+ siderOutboundLogger
57
+ } from "./channel-runtime.js";
58
+ import {
59
+ parseTextFromPart,
60
+ parseMediaFromPart,
61
+ describeInboundMediaReference,
62
+ decodeBase64AttachmentBuffer,
63
+ persistInlineAttachmentBuffer
64
+ } from "./channel-util.js";
65
+ import {
66
+ rememberSiderSessionBinding,
67
+ resolveSiderSessionBinding,
68
+ buildPendingMessageContextForRun,
69
+ consumePendingMessageContextForRun,
70
+ resolvePendingFinalTextForRun,
71
+ updateSiderRunStateContext
72
+ } from "./channel-state.js";
73
+ import { buildTextPart } from "./channel-builders.js";
74
+ import {
75
+ dispatchSiderParts,
76
+ handleSiderSendAction,
77
+ sendToSider
78
+ } from "./channel-send.js";
79
+ import { buildSendMediaResult } from "./channel-send-result.js";
80
+ import { acquireSiderTyping, releaseSiderTyping } from "./channel-typing.js";
81
+ import {
82
+ deliverReplyPayloadToSider,
83
+ persistDeferredFinalReplyPayloadToSider,
84
+ persistStandaloneReplyPayloadToSider,
85
+ enqueueStreamEventTask,
86
+ flushStreamEventQueue,
87
+ handleStreamingPartialSnapshot,
88
+ handleReasoningSnapshot,
89
+ closeStreamingSessionIfActive,
90
+ closeReasoningSessionIfActive,
91
+ commitCurrentStreamTextToFallbackPrefix,
92
+ resetCurrentStreamTextState,
93
+ resetReasoningStreamState,
94
+ rotateAssistantMessageFallbackState,
95
+ resolveFallbackFinalText,
96
+ cleanupSiderRunAfterReply,
97
+ finalizeQueuedRunMessages
98
+ } from "./channel-streaming.js";
99
+ import { runManagedSiderRelayMonitor } from "./channel-monitor.js";
100
+ import { setSiderRuntime } from "./channel-runtime.js";
101
+ import {
102
+ recordSiderPersistedAgentMessage,
103
+ recordSiderLlmOutputUsage,
104
+ recordSiderAgentEnd,
105
+ emitSiderToolHookEvent
106
+ } from "./channel-hooks.js";
107
+ async function handleInboundRealtimeMessage(params) {
108
+ const { cfg, account, event } = params;
109
+ const core = getSiderRuntime();
110
+ const runtimeConfigApi = core.config;
111
+ const currentCfg = runtimeConfigApi.current?.() ?? runtimeConfigApi.loadConfig?.() ?? loadConfig() ?? cfg;
112
+ const sessionId = typeof event.session_id === "string" ? event.session_id.trim() : "";
113
+ if (!sessionId) {
114
+ return;
115
+ }
116
+ if ((event.source_role ?? "").toLowerCase() === "relay") {
117
+ return;
118
+ }
119
+ const parts = Array.isArray(event.parts) ? event.parts : [];
120
+ const textChunks = [];
121
+ const mediaItems = [];
122
+ for (const part of parts) {
123
+ const text = parseTextFromPart(part);
124
+ if (text) {
125
+ textChunks.push(text);
126
+ continue;
127
+ }
128
+ const media = parseMediaFromPart(part);
129
+ if (media) {
130
+ mediaItems.push(media);
131
+ }
132
+ }
133
+ const rawText = textChunks.join("\n").trim();
134
+ const mediaUrls = mediaItems.map((item) => item.url).filter((item) => Boolean(item));
135
+ const parsedMediaTypes = mediaItems.map((item) => item.mimeType).filter((item) => Boolean(item));
136
+ const hasControlCommand = rawText ? core.channel.text.hasControlCommand(rawText, currentCfg) : false;
137
+ const shouldComputeCommandAuthorized = rawText ? core.channel.commands.shouldComputeCommandAuthorized(rawText, currentCfg) : false;
138
+ const commandAuthorized = shouldComputeCommandAuthorized ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
139
+ useAccessGroups: currentCfg.commands?.useAccessGroups !== false,
140
+ authorizers: [{ configured: true, allowed: true }]
141
+ }) : void 0;
142
+ if (!rawText && mediaItems.length === 0) {
143
+ logDebug("drop inbound sider message: no text/media", {
144
+ accountId: account.accountId,
145
+ sessionId,
146
+ messageId: event.message_id,
147
+ partTypes: parts.map((part) => part.type)
148
+ });
149
+ return;
150
+ }
151
+ if (hasControlCommand) {
152
+ logInfo("sider control command inbound", {
153
+ accountId: account.accountId,
154
+ sessionId,
155
+ messageId: event.message_id,
156
+ commandAuthorized
157
+ });
158
+ logDebug("detected sider control command", {
159
+ accountId: account.accountId,
160
+ sessionId,
161
+ messageId: event.message_id,
162
+ commandBody: rawText,
163
+ shouldComputeCommandAuthorized,
164
+ commandAuthorized
165
+ });
166
+ }
167
+ const resolvedInboundMedia = await resolveInboundSiderMedia({
168
+ runtime: core,
169
+ cfg: currentCfg,
170
+ accountId: account.accountId,
171
+ mediaItems,
172
+ gatewayUrl: account.gatewayUrl,
173
+ token: account.token,
174
+ requestTimeoutMs: account.sendTimeoutMs,
175
+ logger: siderOutboundLogger
176
+ });
177
+ const unresolvedMediaUrls = resolvedInboundMedia.unresolvedMedia.map((item) => item.url).filter((item) => Boolean(item));
178
+ const unresolvedMediaRefs = resolvedInboundMedia.unresolvedMedia.map((item) => describeInboundMediaReference(item)).filter((item) => Boolean(item));
179
+ if (resolvedInboundMedia.unresolvedMedia.length > 0) {
180
+ logWarn("sider inbound media fallback to attachment url", {
181
+ accountId: account.accountId,
182
+ sessionId,
183
+ unresolvedCount: resolvedInboundMedia.unresolvedMedia.length,
184
+ unresolvedMediaRefs
185
+ });
186
+ }
187
+ const bodyForAgent = formatTextWithAttachmentLinks(rawText, unresolvedMediaRefs);
188
+ const mediaPayload = resolvedInboundMedia.mediaPayload;
189
+ const route = core.channel.routing.resolveAgentRoute({
190
+ cfg: currentCfg,
191
+ channel: SIDER_CHANNEL_ID,
192
+ accountId: account.accountId,
193
+ peer: {
194
+ kind: "direct",
195
+ id: sessionId
196
+ }
197
+ });
198
+ const to = `session:${sessionId}`;
199
+ const mediaLocalRoots = getAgentScopedMediaLocalRoots(currentCfg, route.agentId);
200
+ const timestamp = typeof event.created_at === "number" && Number.isFinite(event.created_at) ? event.created_at : Date.now();
201
+ const envelopeBody = core.channel.reply.formatAgentEnvelope({
202
+ channel: "Sider",
203
+ from: sessionId,
204
+ timestamp,
205
+ envelope: core.channel.reply.resolveEnvelopeFormatOptions(currentCfg),
206
+ body: bodyForAgent
207
+ });
208
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
209
+ Body: envelopeBody,
210
+ BodyForAgent: bodyForAgent,
211
+ RawBody: bodyForAgent,
212
+ CommandBody: rawText,
213
+ BodyForCommands: rawText,
214
+ From: `${SIDER_CHANNEL_ID}:client:${sessionId}`,
215
+ To: to,
216
+ SessionKey: route.sessionKey,
217
+ AccountId: route.accountId,
218
+ ChatType: "direct",
219
+ ConversationLabel: sessionId,
220
+ SenderId: "client",
221
+ SenderName: "client",
222
+ Provider: SIDER_CHANNEL_ID,
223
+ Surface: SIDER_CHANNEL_ID,
224
+ MessageSid: typeof event.message_id === "string" ? event.message_id : void 0,
225
+ Timestamp: timestamp,
226
+ WasMentioned: true,
227
+ OriginatingChannel: SIDER_CHANNEL_ID,
228
+ OriginatingTo: to,
229
+ ...mediaPayload,
230
+ ...event.meta || {},
231
+ MediaUrl: mediaPayload.MediaUrl || (unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls[0] : void 0),
232
+ MediaUrls: mediaPayload.MediaUrls || (unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls : void 0),
233
+ MediaType: mediaPayload.MediaType || parsedMediaTypes[0],
234
+ MediaTypes: mediaPayload.MediaTypes || (parsedMediaTypes.length > 0 ? parsedMediaTypes : void 0),
235
+ CommandAuthorized: commandAuthorized
236
+ });
237
+ rememberSiderSessionBinding({
238
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
239
+ account,
240
+ sessionId
241
+ });
242
+ const storePath = core.channel.session.resolveStorePath(currentCfg.session?.store, {
243
+ agentId: route.agentId
244
+ });
245
+ const autoTitleConfig = resolveAutoTitleConfig(account.config.autoTitle);
246
+ const autoTitleUserMessage = rawText.slice(0, 500).trim();
247
+ let shouldAutoTitleAfterReply = false;
248
+ if (autoTitleConfig && autoTitleUserMessage) {
249
+ try {
250
+ shouldAutoTitleAfterReply = isFirstTurnInSiderSession({
251
+ cfg: currentCfg,
252
+ agentId: route.agentId,
253
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey
254
+ });
255
+ } catch (error) {
256
+ logWarn("sider auto-title first-turn detection failed", {
257
+ accountId: account.accountId,
258
+ sessionId,
259
+ error: String(error)
260
+ });
261
+ }
262
+ }
263
+ try {
264
+ const modelReset = await maybeResetSiderSessionToCurrentDefaultModel({
265
+ runtime: core,
266
+ cfg: currentCfg,
267
+ agentId: route.agentId,
268
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey
269
+ });
270
+ if (modelReset.changed) {
271
+ logInfo("sider session model reset to current default", {
272
+ accountId: account.accountId,
273
+ sessionId,
274
+ agentId: route.agentId,
275
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
276
+ reason: modelReset.reason,
277
+ previousModel: modelReset.previousModel,
278
+ defaultModel: modelReset.defaultModel
279
+ });
280
+ } else {
281
+ logDebug("sider session model reset skipped", {
282
+ accountId: account.accountId,
283
+ sessionId,
284
+ agentId: route.agentId,
285
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
286
+ reason: modelReset.reason
287
+ });
288
+ }
289
+ } catch (error) {
290
+ logWarn("sider session model reset failed", {
291
+ accountId: account.accountId,
292
+ sessionId,
293
+ agentId: route.agentId,
294
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
295
+ error: String(error)
296
+ });
297
+ }
298
+ await core.channel.session.recordInboundSession({
299
+ storePath,
300
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
301
+ ctx: ctxPayload,
302
+ onRecordError: (err) => {
303
+ logWarn("sider failed to record session", {
304
+ error: String(err)
305
+ });
306
+ }
307
+ });
308
+ const { onModelSelected, ...prefixOptions } = createChannelReplyPipeline({
309
+ cfg: currentCfg,
310
+ agentId: route.agentId,
311
+ channel: SIDER_CHANNEL_ID,
312
+ accountId: account.accountId
313
+ });
314
+ logDebug("dispatching sider inbound message", {
315
+ accountId: account.accountId,
316
+ sessionId,
317
+ messageId: event.message_id,
318
+ routeSessionKey: route.sessionKey,
319
+ hasControlCommand,
320
+ commandAuthorized,
321
+ textLength: rawText.length,
322
+ mediaCount: mediaUrls.length,
323
+ mediaDownloadedCount: mediaPayload.MediaPaths?.length ?? 0,
324
+ mediaDownloadFailedCount: resolvedInboundMedia.unresolvedMedia.length
325
+ });
326
+ const streamState = {
327
+ answer: {
328
+ active: false,
329
+ streamId: void 0,
330
+ seq: 0,
331
+ blockDeltaCount: 0,
332
+ partialDeltaCount: 0,
333
+ streamDeltaMode: "undecided",
334
+ partialSnapshot: "",
335
+ accumulatedBlockText: "",
336
+ assistantSeq: 0,
337
+ fallbackAssistantSeq: 0,
338
+ fallbackTextSource: "none",
339
+ fallbackCurrentText: "",
340
+ fallbackTextPrefix: ""
341
+ },
342
+ reasoning: {
343
+ active: false,
344
+ streamId: void 0,
345
+ seq: 0,
346
+ snapshot: "",
347
+ deltaCount: 0
348
+ },
349
+ persistedFinalText: false,
350
+ pendingFinalPayloads: [],
351
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
352
+ currentRunId: void 0,
353
+ streamEventQueue: Promise.resolve()
354
+ };
355
+ acquireSiderTyping({ account, sessionId });
356
+ let dispatchCompleted = false;
357
+ let dispatchError;
358
+ try {
359
+ try {
360
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
361
+ ctx: ctxPayload,
362
+ cfg: currentCfg,
363
+ dispatcherOptions: {
364
+ ...prefixOptions,
365
+ deliver: async (payload, info) => {
366
+ logDebug("-> deliver: ", payload, info);
367
+ if (payload.isCompactionNotice === true) {
368
+ logDebug("skip sider deliver: compaction notice", { accountId: account.accountId, sessionId, kind: info.kind });
369
+ return;
370
+ }
371
+ if (payload.isError === true && !SIDER_PUSH_TOOL_ERROR_WARNINGS && typeof payload.text === "string" && SIDER_TOOL_ERROR_WARNING_TEXT_RE.test(payload.text)) {
372
+ return;
373
+ }
374
+ logDebug("sider dispatch deliver payload", {
375
+ accountId: account.accountId,
376
+ sessionId,
377
+ kind: info.kind,
378
+ hasText: typeof payload.text === "string" && payload.text.length > 0,
379
+ mediaCount: resolveOutboundMediaUrls(payload).length
380
+ });
381
+ await deliverReplyPayloadToSider({ account, sessionId, streamState, payload, kind: info.kind, mediaLocalRoots });
382
+ },
383
+ onError: (err, info) => {
384
+ logError(`sider ${info.kind} reply failed`, { error: String(err) });
385
+ }
386
+ },
387
+ replyOptions: {
388
+ disableBlockStreaming: false,
389
+ onAgentRunStart: (runId) => {
390
+ streamState.currentRunId = runId;
391
+ const binding = resolveSiderSessionBinding(streamState.sessionKey);
392
+ if (binding) {
393
+ binding.currentRunId = runId;
394
+ }
395
+ updateSiderRunStateContext({
396
+ runId,
397
+ sessionKey: streamState.sessionKey,
398
+ sessionId,
399
+ accountId: account.accountId,
400
+ managedByReplyPipeline: true
401
+ });
402
+ },
403
+ onAssistantMessageStart: () => {
404
+ rotateAssistantMessageFallbackState(streamState);
405
+ },
406
+ onPartialReply: async (payload) => {
407
+ const snapshot = typeof payload.text === "string" ? payload.text : "";
408
+ if (!snapshot || streamState.answer.streamDeltaMode === "block") {
409
+ return;
410
+ }
411
+ if (streamState.answer.streamDeltaMode === "undecided") {
412
+ streamState.answer.streamDeltaMode = "partial";
413
+ }
414
+ await enqueueStreamEventTask({
415
+ streamState,
416
+ task: () => handleStreamingPartialSnapshot({ account, sessionId, streamState, snapshot })
417
+ });
418
+ },
419
+ onReasoningStream: async (payload) => {
420
+ const snapshot = typeof payload.text === "string" ? payload.text : "";
421
+ if (!snapshot.trim()) {
422
+ return;
423
+ }
424
+ await enqueueStreamEventTask({
425
+ streamState,
426
+ task: () => handleReasoningSnapshot({ account, sessionId, streamState, snapshot })
427
+ });
428
+ },
429
+ onReasoningEnd: async () => {
430
+ await enqueueStreamEventTask({
431
+ streamState,
432
+ task: async () => {
433
+ await closeReasoningSessionIfActive({ account, sessionId, streamState, reason: "final", context: "reasoning.done" });
434
+ resetReasoningStreamState(streamState);
435
+ }
436
+ });
437
+ },
438
+ onCompactionStart: async () => {
439
+ await flushStreamEventQueue(streamState);
440
+ commitCurrentStreamTextToFallbackPrefix(streamState);
441
+ await closeStreamingSessionIfActive({ account, sessionId, streamState, reason: "interrupted", context: "stream.done.compaction-start" });
442
+ resetCurrentStreamTextState(streamState);
443
+ await closeReasoningSessionIfActive({ account, sessionId, streamState, reason: "interrupted", context: "reasoning.done.compaction-start" });
444
+ resetReasoningStreamState(streamState);
445
+ logDebug("sider stream state reset for compaction", {
446
+ accountId: account.accountId,
447
+ sessionId,
448
+ fallbackPrefixLength: streamState.answer.fallbackTextPrefix.length,
449
+ reasoningTextLength: streamState.reasoning.snapshot.length
450
+ });
451
+ },
452
+ onModelSelected
453
+ }
454
+ });
455
+ dispatchCompleted = true;
456
+ logDebug("completed sider inbound dispatch", { accountId: account.accountId, sessionId, messageId: event.message_id });
457
+ } catch (err) {
458
+ dispatchError = err;
459
+ } finally {
460
+ await flushStreamEventQueue(streamState);
461
+ const streamEndedNormally = dispatchCompleted;
462
+ await closeStreamingSessionIfActive({
463
+ account,
464
+ sessionId,
465
+ streamState,
466
+ reason: streamEndedNormally ? "final" : "interrupted",
467
+ context: streamEndedNormally ? "stream.done.dispatch-end" : "stream.done.interrupted"
468
+ });
469
+ await closeReasoningSessionIfActive({
470
+ account,
471
+ sessionId,
472
+ streamState,
473
+ reason: streamEndedNormally ? "final" : "interrupted",
474
+ context: streamEndedNormally ? "reasoning.done.dispatch-end" : "reasoning.done.interrupted"
475
+ });
476
+ streamState.answer.blockDeltaCount = 0;
477
+ resetReasoningStreamState(streamState);
478
+ }
479
+ if (!dispatchCompleted) {
480
+ const selectedFinalText = resolvePendingFinalTextForRun(streamState.currentRunId) || resolveFallbackFinalText(streamState).trim();
481
+ const messageContext = buildPendingMessageContextForRun({
482
+ accountId: account.accountId,
483
+ runId: streamState.currentRunId,
484
+ includeFinalThinking: true
485
+ });
486
+ const cleanupRunId = messageContext.runId ?? streamState.currentRunId;
487
+ const finalParts = [
488
+ ...messageContext.parts,
489
+ ...selectedFinalText ? [buildTextPart(selectedFinalText)] : []
490
+ ];
491
+ if (finalParts.length > 0 || cleanupRunId) {
492
+ logDebug("dispatch ended with interruption; finalizing current assistant text", {
493
+ accountId: account.accountId,
494
+ sessionId,
495
+ runId: cleanupRunId,
496
+ finalTextLength: selectedFinalText.length,
497
+ partTypes: finalParts.map((part) => part.type)
498
+ });
499
+ await finalizeQueuedRunMessages({
500
+ account,
501
+ sessionId,
502
+ streamState,
503
+ parts: finalParts,
504
+ stopReason: messageContext.stopReason,
505
+ context: "final.interrupted"
506
+ });
507
+ consumePendingMessageContextForRun({
508
+ runId: cleanupRunId,
509
+ consumeFinalThinking: true
510
+ });
511
+ cleanupSiderRunAfterReply({ streamState, runId: cleanupRunId });
512
+ } else {
513
+ cleanupSiderRunAfterReply({ streamState, runId: streamState.currentRunId });
514
+ }
515
+ if (dispatchError) {
516
+ throw dispatchError;
517
+ }
518
+ return;
519
+ }
520
+ const pendingFinalPayloads = [...streamState.pendingFinalPayloads];
521
+ streamState.pendingFinalPayloads = [];
522
+ if (pendingFinalPayloads.length > 0) {
523
+ const pendingRunFinalText = resolvePendingFinalTextForRun(streamState.currentRunId).trim();
524
+ const fallbackFinalText = (pendingRunFinalText || resolveFallbackFinalText(streamState)).trim();
525
+ let primaryFinalPayloadIndex = pendingFinalPayloads.findIndex(
526
+ (payload) => payload.isError !== true
527
+ );
528
+ const fallbackMatchingPayloadIndex = primaryFinalPayloadIndex < 0 && fallbackFinalText.length > 0 ? pendingFinalPayloads.findIndex((payload) => {
529
+ const payloadText = typeof payload.text === "string" ? payload.text.trim() : "";
530
+ return payloadText.length > 0 && payloadText === fallbackFinalText;
531
+ }) : -1;
532
+ if (primaryFinalPayloadIndex < 0 && fallbackMatchingPayloadIndex >= 0) {
533
+ primaryFinalPayloadIndex = fallbackMatchingPayloadIndex;
534
+ logDebug("promoting matching final payload as primary sider message", {
535
+ accountId: account.accountId,
536
+ sessionId,
537
+ runId: streamState.currentRunId,
538
+ primaryFinalPayloadIndex,
539
+ isError: pendingFinalPayloads[primaryFinalPayloadIndex]?.isError === true,
540
+ textLength: fallbackFinalText.length
541
+ });
542
+ }
543
+ const shouldSynthesizePrimaryFinal = primaryFinalPayloadIndex < 0 && fallbackFinalText.length > 0 && (pendingRunFinalText.length > 0 || streamState.answer.fallbackTextSource !== "final_payload");
544
+ if (!shouldSynthesizePrimaryFinal && primaryFinalPayloadIndex < 0) {
545
+ logDebug("skipping synthetic primary final message for payload-derived fallback", {
546
+ accountId: account.accountId,
547
+ sessionId,
548
+ runId: streamState.currentRunId,
549
+ fallbackTextSource: streamState.answer.fallbackTextSource,
550
+ fallbackTextLength: fallbackFinalText.length,
551
+ pendingFinalPayloadCount: pendingFinalPayloads.length
552
+ });
553
+ primaryFinalPayloadIndex = 0;
554
+ }
555
+ if (shouldSynthesizePrimaryFinal) {
556
+ const messageContext = buildPendingMessageContextForRun({
557
+ accountId: account.accountId,
558
+ runId: streamState.currentRunId,
559
+ includeFinalThinking: true
560
+ });
561
+ const cleanupRunId = messageContext.runId ?? streamState.currentRunId;
562
+ const finalParts = [
563
+ ...messageContext.parts,
564
+ ...fallbackFinalText ? [buildTextPart(fallbackFinalText)] : []
565
+ ];
566
+ if (finalParts.length > 0 || cleanupRunId) {
567
+ logDebug("synthesizing primary final message before standalone error payloads", {
568
+ accountId: account.accountId,
569
+ sessionId,
570
+ runId: cleanupRunId,
571
+ finalTextLength: fallbackFinalText.length,
572
+ extraFinalPayloadCount: pendingFinalPayloads.length
573
+ });
574
+ await finalizeQueuedRunMessages({
575
+ account,
576
+ sessionId,
577
+ streamState,
578
+ parts: finalParts,
579
+ stopReason: messageContext.stopReason,
580
+ context: "final.synthetic_primary"
581
+ });
582
+ consumePendingMessageContextForRun({
583
+ runId: cleanupRunId,
584
+ consumeFinalThinking: true
585
+ });
586
+ cleanupSiderRunAfterReply({ streamState, runId: cleanupRunId });
587
+ }
588
+ }
589
+ for (let index = 0; index < pendingFinalPayloads.length; index += 1) {
590
+ const payload = pendingFinalPayloads[index];
591
+ if (!shouldSynthesizePrimaryFinal && index === primaryFinalPayloadIndex) {
592
+ await persistDeferredFinalReplyPayloadToSider({
593
+ account,
594
+ sessionId,
595
+ streamState,
596
+ payload,
597
+ mediaLocalRoots
598
+ });
599
+ continue;
600
+ }
601
+ await persistStandaloneReplyPayloadToSider({
602
+ account,
603
+ sessionId,
604
+ payload,
605
+ mediaLocalRoots
606
+ });
607
+ }
608
+ }
609
+ if (!streamState.persistedFinalText) {
610
+ const selectedFinalText = resolvePendingFinalTextForRun(streamState.currentRunId) || resolveFallbackFinalText(streamState).trim();
611
+ const messageContext = buildPendingMessageContextForRun({
612
+ accountId: account.accountId,
613
+ runId: streamState.currentRunId,
614
+ includeFinalThinking: true
615
+ });
616
+ const cleanupRunId = messageContext.runId ?? streamState.currentRunId;
617
+ const finalParts = [
618
+ ...messageContext.parts,
619
+ ...selectedFinalText ? [buildTextPart(selectedFinalText)] : []
620
+ ];
621
+ if (finalParts.length > 0 || cleanupRunId) {
622
+ logDebug(
623
+ selectedFinalText ? "no final payload observed; finalizing current assistant text" : "no final payload observed; flushing queued tail as final message",
624
+ {
625
+ accountId: account.accountId,
626
+ sessionId,
627
+ runId: cleanupRunId,
628
+ finalTextLength: selectedFinalText.length,
629
+ partTypes: finalParts.map((part) => part.type)
630
+ }
631
+ );
632
+ await finalizeQueuedRunMessages({
633
+ account,
634
+ sessionId,
635
+ streamState,
636
+ parts: finalParts,
637
+ stopReason: messageContext.stopReason,
638
+ context: selectedFinalText ? "final.text_fallback" : "final.queued_tail"
639
+ });
640
+ consumePendingMessageContextForRun({
641
+ runId: cleanupRunId,
642
+ consumeFinalThinking: true
643
+ });
644
+ cleanupSiderRunAfterReply({ streamState, runId: cleanupRunId });
645
+ } else {
646
+ cleanupSiderRunAfterReply({ streamState, runId: streamState.currentRunId });
647
+ }
648
+ }
649
+ if (streamState.persistedFinalText && shouldAutoTitleAfterReply && autoTitleConfig) {
650
+ scheduleSiderAutoTitleUpdate({
651
+ cfg: currentCfg,
652
+ account,
653
+ sessionId,
654
+ userMessage: autoTitleUserMessage,
655
+ prompt: autoTitleConfig.prompt,
656
+ agentId: route.agentId
657
+ });
658
+ }
659
+ } finally {
660
+ releaseSiderTyping({ account, sessionId });
661
+ }
662
+ }
663
+ const siderPlugin = {
664
+ id: SIDER_CHANNEL_ID,
665
+ ...siderChannelPluginCommon,
666
+ agentPrompt: {
667
+ messageToolHints: () => [
668
+ "- Sider targeting: omit `to` to reply in the current conversation (auto-inferred).",
669
+ "- To send an existing image or file to the current user, prefer the message tool with action='send' and set 'media' to a local file path or a remote HTTPS URL. You do not need to specify 'to' \u2014 the current conversation recipient is used automatically.",
670
+ "- IMPORTANT: Use action='send' for files/images. action='sendAttachment' is limited.",
671
+ "- When the user asks you to find an image from the web, use a web search or browser tool to find a suitable image URL, then send it using the message tool with 'media' set to that HTTPS image URL \u2014 do NOT download the image first.",
672
+ "- If the user asks you to download, export, or otherwise produce a file, send the resulting file to the user with the message tool. Do not stop at describing the file or giving its path.",
673
+ // "- If you call image_generate, the generated image is sent to the user automatically. Do not send the same image again with the message tool unless the user explicitly asks for a separate resend.",
674
+ "- IMPORTANT: When generating or saving a file to send, always use an absolute path (e.g. /tmp/photo.png), never a relative path like ./photo.png. Relative paths cannot be resolved and the file will not be delivered."
675
+ ]
676
+ },
677
+ streaming: {
678
+ blockStreamingCoalesceDefaults: { minChars: 1200, idleMs: 900 }
679
+ },
680
+ actions: {
681
+ describeMessageTool: ({ cfg }) => {
682
+ const hasConfigured = listSiderAccountIds(cfg).some((accountId) => {
683
+ const account = resolveSiderAccount(cfg, accountId);
684
+ return account.configured || isSiderAccountBootstrappable(account);
685
+ });
686
+ return hasConfigured ? { actions: ["send"] } : null;
687
+ },
688
+ supportsAction: ({ action }) => action === "send",
689
+ extractToolSend: ({ args }) => extractSiderToolSend(args),
690
+ handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots }) => {
691
+ logDebug(">> handleAction:", action, params);
692
+ if (action === "send") {
693
+ return jsonResult(
694
+ await handleSiderSendAction({
695
+ cfg,
696
+ accountId,
697
+ params,
698
+ mediaLocalRoots
699
+ })
700
+ );
701
+ }
702
+ if (action !== "sendAttachment") {
703
+ throw new Error(`sider action ${action} is not supported`);
704
+ }
705
+ const account = await resolveManagedSiderAccount({ cfg, accountId });
706
+ if (!account.configured) {
707
+ throw new Error(describeSiderAccountConfigurationError(account));
708
+ }
709
+ const to = readStringParam(params, "to", { required: true });
710
+ const rawBuffer = readStringParam(params, "buffer", { trim: false, required: true });
711
+ const contentType = readStringParam(params, "contentType", { trim: false }) ?? readStringParam(params, "mimeType", { trim: false });
712
+ const fileName = readStringParam(params, "filename", { trim: false }) ?? void 0;
713
+ const sessionId = resolveOutboundSessionId({ account, to });
714
+ const buffer = decodeBase64AttachmentBuffer(rawBuffer);
715
+ const savedAttachment = await persistInlineAttachmentBuffer({
716
+ cfg,
717
+ accountId,
718
+ buffer,
719
+ contentType: contentType ?? void 0,
720
+ fileName
721
+ });
722
+ const resolvedContentType = contentType ?? savedAttachment.contentType ?? void 0;
723
+ const resolvedFileName = fileName ?? savedAttachment.path;
724
+ const preparedAttachment = await buildOptimisticSiderPartFromInlineAttachment({
725
+ account,
726
+ sessionId,
727
+ buffer,
728
+ contentType: resolvedContentType,
729
+ fileName: resolvedFileName,
730
+ source: resolvedFileName
731
+ });
732
+ const sendParts = [preparedAttachment.part];
733
+ const attachmentResult = await buildSendMediaResult({
734
+ account,
735
+ sessionId,
736
+ part: preparedAttachment.part,
737
+ source: savedAttachment.path,
738
+ path: savedAttachment.path
739
+ });
740
+ await dispatchSiderParts({
741
+ account,
742
+ sessionId,
743
+ parts: sendParts,
744
+ deferredUploads: preparedAttachment.deferredUpload ? [preparedAttachment.deferredUpload] : [],
745
+ allowMergeActiveRun: true,
746
+ context: "action.sendAttachment"
747
+ });
748
+ return jsonResult(attachmentResult);
749
+ }
750
+ },
751
+ outbound: {
752
+ deliveryMode: "direct",
753
+ sendText: async ({ cfg, accountId, to, text, mediaLocalRoots }) => {
754
+ logDebug("-> sendText:", text);
755
+ const result = await sendToSider({ cfg, accountId, to, text, mediaLocalRoots, errorHint: "sider sendText requires non-empty text" });
756
+ return { channel: SIDER_CHANNEL_ID, messageId: result.messageId, conversationId: result.conversationId };
757
+ },
758
+ sendMedia: async ({ cfg, accountId, to, text, mediaUrl, mediaLocalRoots }) => {
759
+ logDebug("-> sendMedia:", text, mediaUrl);
760
+ const result = await sendToSider({ cfg, accountId, to, text, mediaUrl, mediaLocalRoots, errorHint: "sider sendMedia requires text and/or mediaUrl" });
761
+ return { channel: SIDER_CHANNEL_ID, messageId: result.messageId, conversationId: result.conversationId };
762
+ }
763
+ },
764
+ gateway: {
765
+ startAccount: async (ctx) => {
766
+ const account = await resolveManagedSiderAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
767
+ if (!account.configured) {
768
+ throw new Error(describeSiderAccountConfigurationError(account));
769
+ }
770
+ ctx.log?.info(`[${account.accountId}] starting sider relay monitor (${account.gatewayUrl}, relayId=${account.relayId})`);
771
+ await runManagedSiderRelayMonitor({
772
+ cfg: ctx.cfg,
773
+ account,
774
+ abortSignal: ctx.abortSignal,
775
+ log: ctx.log,
776
+ onInboundMessage: handleInboundRealtimeMessage
777
+ });
778
+ ctx.log?.info(`[${account.accountId}] sider relay monitor watcher stopped`);
779
+ }
780
+ },
781
+ auth: {
782
+ login: async ({ accountId, runtime }) => {
783
+ const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
784
+ for (; ; ) {
785
+ const pairing = await requestSiderPairing();
786
+ runtime.log(
787
+ formatSiderPairingInstructions({ pairingCode: pairing.pairingCode, leadLine: "One more step to connect:" })
788
+ );
789
+ const reportPendingUpdate = createSiderPairingPendingUpdateReporter({
790
+ pairingCode: pairing.pairingCode,
791
+ report: (message) => {
792
+ runtime.log(message);
793
+ }
794
+ });
795
+ try {
796
+ const paired = await waitForSiderPairing({
797
+ pairing,
798
+ onPending: reportPendingUpdate,
799
+ onRetryableError: (message) => {
800
+ runtime.log(message);
801
+ }
802
+ });
803
+ const latestCfg = loadConfig();
804
+ const nextCfg = applySiderSetupAccountConfig({ cfg: latestCfg, accountId: resolvedAccountId, input: { token: paired.token } });
805
+ await writeConfigFile(nextCfg);
806
+ runtime.log("Connected! You can now chat with me in the browser Side Panel.");
807
+ return;
808
+ } catch (error) {
809
+ if (error instanceof SiderPairingExpiredError) {
810
+ runtime.log("Pairing code expired.");
811
+ runtime.log("Generating a new code...");
812
+ continue;
813
+ }
814
+ throw error;
815
+ }
816
+ }
817
+ }
818
+ },
819
+ messaging: {
820
+ normalizeTarget: normalizeSiderMessagingTarget,
821
+ targetResolver: {
822
+ looksLikeId: looksLikeSiderTargetId,
823
+ hint: "session:<sessionId> (or raw <sessionId>)"
824
+ }
825
+ }
826
+ };
827
+ export {
828
+ emitSiderToolHookEvent,
829
+ recordSiderAgentEnd,
830
+ recordSiderLlmOutputUsage,
831
+ recordSiderPersistedAgentMessage,
832
+ setSiderRuntime,
833
+ siderPlugin
834
+ };