r2-relay-channel 0.4.0 → 0.4.2

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.
package/dist/config.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
3
- export declare const DEFAULT_POLL_INTERVAL_MS = 5000;
3
+ export declare const DEFAULT_POLL_INTERVAL_MS = 3000;
4
+ export declare const MIN_POLL_INTERVAL_MS = 2000;
5
+ export declare const MAX_POLL_INTERVAL_MS = 60000;
4
6
  export declare const DEFAULT_BACKOFF_MAX_MS = 40000;
5
7
  export declare const DEFAULT_TTL_DAYS = 7;
6
8
  export declare const DEFAULT_IDENTITY_TTL_DAYS = 1;
@@ -16,7 +18,7 @@ export declare const R2RelaySidecarConfigSchema: z.ZodObject<{
16
18
  accessKeyId: z.ZodString;
17
19
  secretAccessKey: z.ZodString;
18
20
  serverId: z.ZodOptional<z.ZodString>;
19
- pollIntervalMs: z.ZodOptional<z.ZodNumber>;
21
+ pollIntervalMs: z.ZodOptional<z.ZodUnknown>;
20
22
  backoffMaxMs: z.ZodOptional<z.ZodNumber>;
21
23
  defaultTtlDays: z.ZodOptional<z.ZodNumber>;
22
24
  ttl: z.ZodOptional<z.ZodObject<{
@@ -70,6 +72,7 @@ export declare function resolveDefaultServerId(_cfg: OpenClawConfig): string;
70
72
  export declare function normalizeServerId(value?: string | null): string | null;
71
73
  export declare function resolveRelayConfigFilePath(configFile?: string | null): string;
72
74
  export declare function loadR2RelaySidecarConfig(configFile: string): R2RelayAccountConfig;
75
+ export declare function normalizePollIntervalMs(value: unknown): number;
73
76
  export declare function resolveR2RelayAccount(params: {
74
77
  cfg: OpenClawConfig;
75
78
  accountId?: string | null;
package/dist/config.js CHANGED
@@ -5,7 +5,9 @@ import { z } from "zod";
5
5
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
6
6
  import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
7
7
  import { getPersistedRelayServerId } from "./runtime.js";
8
- export const DEFAULT_POLL_INTERVAL_MS = 5_000;
8
+ export const DEFAULT_POLL_INTERVAL_MS = 3_000;
9
+ export const MIN_POLL_INTERVAL_MS = 2_000;
10
+ export const MAX_POLL_INTERVAL_MS = 60_000;
9
11
  export const DEFAULT_BACKOFF_MAX_MS = 40_000;
10
12
  export const DEFAULT_TTL_DAYS = 7;
11
13
  export const DEFAULT_IDENTITY_TTL_DAYS = 1;
@@ -23,7 +25,7 @@ export const R2RelaySidecarConfigSchema = z.object({
23
25
  accessKeyId: z.string().min(1),
24
26
  secretAccessKey: z.string().min(1),
25
27
  serverId: z.string().trim().min(1).optional(),
26
- pollIntervalMs: z.number().int().positive().optional(),
28
+ pollIntervalMs: z.unknown().optional(),
27
29
  backoffMaxMs: z.number().int().positive().optional(),
28
30
  defaultTtlDays: z.number().positive().optional(),
29
31
  ttl: z.object({
@@ -39,6 +41,7 @@ export const r2RelayChannelConfigSchema = buildJsonChannelConfigSchema({
39
41
  properties: {
40
42
  enabled: { type: "boolean" },
41
43
  configFile: { type: "string", minLength: 1 },
44
+ pollIntervalMs: { type: "number" },
42
45
  },
43
46
  });
44
47
  export function getR2RelayConfig(cfg) {
@@ -80,16 +83,31 @@ export function loadR2RelaySidecarConfig(configFile) {
80
83
  return {};
81
84
  }
82
85
  }
86
+ export function normalizePollIntervalMs(value) {
87
+ if (typeof value !== "number" || !Number.isFinite(value)) {
88
+ return DEFAULT_POLL_INTERVAL_MS;
89
+ }
90
+ return Math.min(MAX_POLL_INTERVAL_MS, Math.max(MIN_POLL_INTERVAL_MS, Math.round(value)));
91
+ }
83
92
  export function resolveR2RelayAccount(params) {
84
93
  const relayCfg = getR2RelayConfig(params.cfg) ?? {};
85
94
  const configFile = resolveRelayConfigFilePath(relayCfg.configFile);
86
95
  const rawFileCfg = loadR2RelaySidecarConfig(configFile);
87
96
  const parsedFileCfg = R2RelaySidecarConfigSchema.safeParse(rawFileCfg);
88
- const fileCfg = parsedFileCfg.success ? parsedFileCfg.data : {};
97
+ let fileCfg = {};
98
+ if (parsedFileCfg.success) {
99
+ fileCfg = {
100
+ ...parsedFileCfg.data,
101
+ pollIntervalMs: typeof parsedFileCfg.data.pollIntervalMs === "number"
102
+ ? parsedFileCfg.data.pollIntervalMs
103
+ : undefined,
104
+ };
105
+ }
89
106
  const mergedCfg = {
90
107
  ...fileCfg,
91
108
  enabled: relayCfg.enabled ?? fileCfg.enabled,
92
109
  configFile,
110
+ pollIntervalMs: relayCfg.pollIntervalMs ?? fileCfg.pollIntervalMs,
93
111
  };
94
112
  const endpoint = mergedCfg.endpoint?.trim() ?? "";
95
113
  const bucket = mergedCfg.bucket?.trim() ?? "";
@@ -114,7 +132,7 @@ export function resolveR2RelayAccount(params) {
114
132
  serverId: normalizeServerId(mergedCfg.serverId) ?? resolveDefaultServerId(params.cfg),
115
133
  region: DEFAULT_REGION,
116
134
  forcePathStyle: DEFAULT_FORCE_PATH_STYLE,
117
- pollIntervalMs: mergedCfg.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
135
+ pollIntervalMs: normalizePollIntervalMs(mergedCfg.pollIntervalMs),
118
136
  backoffMaxMs: mergedCfg.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
119
137
  defaultTtlDays: mergedCfg.defaultTtlDays ?? DEFAULT_TTL_DAYS,
120
138
  ttl,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, normalizePollIntervalMs, resolveR2RelayAccount, } from "./config.js";
7
+ test("poll interval defaults to 3 seconds and clamps to the supported range", () => {
8
+ assert.equal(DEFAULT_POLL_INTERVAL_MS, 3_000);
9
+ assert.equal(normalizePollIntervalMs(undefined), 3_000);
10
+ assert.equal(normalizePollIntervalMs(1_000), MIN_POLL_INTERVAL_MS);
11
+ assert.equal(normalizePollIntervalMs(90_000), MAX_POLL_INTERVAL_MS);
12
+ });
13
+ test("invalid optional poll interval does not invalidate relay credentials", (t) => {
14
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "r2-relay-config-"));
15
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
16
+ const configFile = path.join(directory, "r2relay.config.json");
17
+ fs.writeFileSync(configFile, JSON.stringify({
18
+ endpoint: "https://example.r2.cloudflarestorage.com",
19
+ bucket: "relay-bucket",
20
+ accessKeyId: "access-key",
21
+ secretAccessKey: "secret-key",
22
+ serverId: "relay-server",
23
+ pollIntervalMs: "invalid",
24
+ }));
25
+ const account = resolveR2RelayAccount({
26
+ cfg: {
27
+ channels: {
28
+ "r2-relay-channel": { enabled: true, configFile },
29
+ },
30
+ },
31
+ });
32
+ assert.equal(account.configured, true);
33
+ assert.equal(account.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
34
+ });
package/dist/inbound.js CHANGED
@@ -264,12 +264,16 @@ export async function dispatchInboundMessage(params) {
264
264
  textHasMediaDirective: text.includes("MEDIA:"),
265
265
  });
266
266
  if (streamState.active && text.trim().length > 0) {
267
- if (streamState.lastText && !text.startsWith(streamState.lastText)) {
268
- return;
269
- }
270
267
  streamState.lastText = text;
271
268
  streamState.lastEmitAt = Date.now();
272
269
  }
270
+ const finalStream = streamState.active
271
+ ? {
272
+ stream_id: streamState.streamId,
273
+ seq: streamState.seq + 1,
274
+ state: "final",
275
+ }
276
+ : undefined;
273
277
  try {
274
278
  await sendRelayPayloadMessage({
275
279
  cfg: params.cfg,
@@ -287,6 +291,7 @@ export async function dispatchInboundMessage(params) {
287
291
  meta: {
288
292
  route: outboundMeta.route,
289
293
  workspaceDir: resolveAgentWorkspaceDirFromConfig(params.cfg, route.agentId),
294
+ stream: finalStream,
290
295
  },
291
296
  });
292
297
  emitRelayDebug(params.log, `[${params.account.accountId}] sent relay final reply`, {
@@ -13,13 +13,8 @@
13
13
  "additionalProperties": false,
14
14
  "properties": {
15
15
  "enabled": { "type": "boolean" },
16
- "configFile": { "type": "string" }
17
- }
18
- },
19
- "uiHints": {
20
- "configFile": {
21
- "label": "Config file",
22
- "placeholder": "/path/to/r2relay.config.json"
16
+ "configFile": { "type": "string" },
17
+ "pollIntervalMs": { "type": "number" }
23
18
  }
24
19
  }
25
20
  }
@@ -33,7 +28,8 @@
33
28
  "additionalProperties": false,
34
29
  "properties": {
35
30
  "enabled": { "type": "boolean" },
36
- "configFile": { "type": "string" }
31
+ "configFile": { "type": "string" },
32
+ "pollIntervalMs": { "type": "number" }
37
33
  }
38
34
  }
39
35
  }
@@ -19,6 +19,11 @@ export declare function sendRelayPayloadMessage(params: {
19
19
  mediaAccess?: OutboundMediaAccess;
20
20
  mediaLocalRoots?: readonly string[];
21
21
  mediaReadFile?: (filePath: string) => Promise<Buffer>;
22
+ stream?: {
23
+ stream_id: string;
24
+ seq: number;
25
+ state: "partial" | "final";
26
+ };
22
27
  };
23
28
  }): Promise<{
24
29
  channel: string;
package/dist/outbound.js CHANGED
@@ -54,6 +54,7 @@ export async function sendRelayPayloadMessage(params) {
54
54
  const result = await service.sendMessage(target.peer, {
55
55
  route,
56
56
  content: relayContentFromPayload(fallbackText, attachments, params.payload.channelData),
57
+ delivery: params.meta?.stream ? { stream: params.meta.stream } : undefined,
57
58
  });
58
59
  emitRelayDebug(params.log, `[${account.accountId}] outbound relay message sent`, {
59
60
  source: params.source ?? "unknown",
package/dist/service.js CHANGED
@@ -117,13 +117,13 @@ export class Service {
117
117
  software: { id: "openclaw", name: "OpenClaw", version: RELAY_PLUGIN_VERSION },
118
118
  protocol: { name: "r2-relay", version: 3 },
119
119
  capabilities: {
120
- messaging: { text: true, streaming: true, reactions: true, system_events: true },
120
+ messaging: { text: true, streaming: true, reactions: true, system_events: false },
121
121
  conversations: { list: true, create: false, reset: false, archive: false, threading: false },
122
122
  agents: { list: true, multiple: true, switch: true, per_agent_models: false },
123
123
  attachments: null,
124
124
  approvals: {
125
125
  exec: true,
126
- tool: false,
126
+ tool: true,
127
127
  custom: false,
128
128
  },
129
129
  extensions: null,
@@ -3,5 +3,6 @@ import type { ResolvedR2RelayAccount } from "./config.js";
3
3
  import type { ConversationDescriptor } from "./protocol.js";
4
4
  export declare function syncPublishedIdentity(service: Service, account: ResolvedR2RelayAccount, force?: boolean): Promise<void>;
5
5
  export declare function schedulePublishedIdentityRefresh(service: Service, account: ResolvedR2RelayAccount, delayMs?: number): void;
6
+ export declare function shouldPublishIdentitySession(sessionKey: string): boolean;
6
7
  export declare function agentIdFromSessionKey(sessionKey: string): string;
7
8
  export declare function readPublishedConversationEntry(sessionKey: string): Promise<ConversationDescriptor | null>;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { maxBytesForKind } from "openclaw/plugin-sdk/media-runtime";
3
+ import { isAcpSessionKey, isCronSessionKey, isSubagentSessionKey } from "openclaw/plugin-sdk/routing";
3
4
  import { getRelayConfig, getRelayRuntime } from "./runtime.js";
4
5
  import { IDENTITY_REFRESH_INTERVAL_MS, RELAY_PLUGIN_VERSION } from "./service.js";
5
6
  const MAX_IMAGE_BYTES = maxBytesForKind("image");
@@ -28,7 +29,7 @@ export async function syncPublishedIdentity(service, account, force = false) {
28
29
  protocol: { name: "r2-relay", version: 3 },
29
30
  software: { id: "openclaw", name: "OpenClaw", version: RELAY_PLUGIN_VERSION },
30
31
  capabilities: {
31
- messaging: { text: true, streaming: true, reactions: true, system_events: true },
32
+ messaging: { text: true, streaming: true, reactions: true, system_events: false },
32
33
  conversations: { list: true, create: false, reset: false, archive: false, threading: false },
33
34
  agents: { list: true, multiple: agents.length > 1, switch: true, per_agent_models: Boolean(modelCapabilities) },
34
35
  attachments: {
@@ -39,7 +40,7 @@ export async function syncPublishedIdentity(service, account, force = false) {
39
40
  },
40
41
  approvals: {
41
42
  exec: true,
42
- tool: false,
43
+ tool: true,
43
44
  custom: false,
44
45
  },
45
46
  extensions: null,
@@ -150,12 +151,12 @@ async function collectPublishedConversations(_account) {
150
151
  conversations: conversations.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0)),
151
152
  };
152
153
  }
153
- function shouldPublishIdentitySession(sessionKey) {
154
+ export function shouldPublishIdentitySession(sessionKey) {
154
155
  const normalized = sessionKey.trim().toLowerCase();
155
156
  if (!normalized) {
156
157
  return false;
157
158
  }
158
- if (normalized.startsWith("cron:")) {
159
+ if (isCronSessionKey(normalized) || isSubagentSessionKey(normalized) || isAcpSessionKey(normalized)) {
159
160
  return false;
160
161
  }
161
162
  if (normalized.startsWith("dreaming-")) {
@@ -164,7 +165,10 @@ function shouldPublishIdentitySession(sessionKey) {
164
165
  const parts = normalized.split(":");
165
166
  if (parts.length >= 3 && parts[0] === "agent") {
166
167
  const scopedKey = parts.slice(2).join(":");
167
- if (scopedKey.startsWith("cron:")) {
168
+ if (scopedKey.startsWith("dreaming-")) {
169
+ return false;
170
+ }
171
+ if (isCronSessionKey(scopedKey) || isSubagentSessionKey(scopedKey) || isAcpSessionKey(scopedKey)) {
168
172
  return false;
169
173
  }
170
174
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { shouldPublishIdentitySession } from "./session-publication.js";
4
+ test("identity publishes interactive conversations but not runtime execution sessions", () => {
5
+ assert.equal(shouldPublishIdentitySession("agent:main:telegram:direct:user-1"), true);
6
+ assert.equal(shouldPublishIdentitySession("agent:main:discord:channel:room-1:thread:topic-1"), true);
7
+ assert.equal(shouldPublishIdentitySession("agent:main:cron:nightly"), false);
8
+ assert.equal(shouldPublishIdentitySession("agent:main:subagent:worker-1"), false);
9
+ assert.equal(shouldPublishIdentitySession("agent:main:acp:run-1"), false);
10
+ assert.equal(shouldPublishIdentitySession("agent:main:dreaming-summary"), false);
11
+ });
package/dist/setup.js CHANGED
@@ -171,8 +171,6 @@ export const r2RelaySetupWizard = {
171
171
  status: {
172
172
  configuredLabel: "Configured",
173
173
  unconfiguredLabel: "Not configured",
174
- configuredHint: "R2 endpoint, bucket, and credentials are present.",
175
- unconfiguredHint: "Add your R2 bucket details to enable the dedicated OpenClaw app flow.",
176
174
  resolveConfigured: ({ cfg }) => {
177
175
  const section = getSidecarSection(cfg);
178
176
  return Boolean(section.endpoint?.trim() &&
@@ -193,7 +191,6 @@ export const r2RelaySetupWizard = {
193
191
  if (section.serverId?.trim()) {
194
192
  lines.push(`Server ID: ${section.serverId.trim()}`);
195
193
  }
196
- lines.push(`Config file: ${getConfigFilePath(cfg)}`);
197
194
  return lines;
198
195
  },
199
196
  },
@@ -13,13 +13,8 @@
13
13
  "additionalProperties": false,
14
14
  "properties": {
15
15
  "enabled": { "type": "boolean" },
16
- "configFile": { "type": "string" }
17
- }
18
- },
19
- "uiHints": {
20
- "configFile": {
21
- "label": "Config file",
22
- "placeholder": "/path/to/r2relay.config.json"
16
+ "configFile": { "type": "string" },
17
+ "pollIntervalMs": { "type": "number" }
23
18
  }
24
19
  }
25
20
  }
@@ -33,7 +28,8 @@
33
28
  "additionalProperties": false,
34
29
  "properties": {
35
30
  "enabled": { "type": "boolean" },
36
- "configFile": { "type": "string" }
31
+ "configFile": { "type": "string" },
32
+ "pollIntervalMs": { "type": "number" }
37
33
  }
38
34
  }
39
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "r2-relay-channel",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "R2 Relay Channel is a secure, decentralized OpenClaw messaging channel that keeps delivery simple while preserving data ownership without relying on an IM provider.",
5
5
  "type": "module",
6
6
  "main": "dist/r2-relay.js",