r2-relay-channel 0.4.1 → 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/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.1",
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",