@clawrent/openclaw-channel 0.5.0 → 0.6.0

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,17 +1,16 @@
1
- /** @-routing policy (spec §6.3 + Plan 4b-2.2).
1
+ /** Is this provider agent @-addressed by the inbound group message? (D1: was a
2
+ * wake/sleep GATE — now CONTEXT only; the agent self-decides whether to reply.)
2
3
  *
3
- * Decides whether THIS provider agent should wake (drive the OpenClaw bot) for an
4
- * inbound group message:
5
- * - No `mentions` wake. Backward-compatible with the single-agent /ws/session
6
- * plugin (where every consumer message is answered). Also the sensible default
7
- * when a session has a single provider agent.
8
- * - `mentions` present wake ONLY if this agent's participantId is in the list.
9
- * Non-@-ed agents stay silent (spec §6.3). If our participantId is unknown
10
- * (handshake not yet cached), we cannot match → sleep (safer than guessing).
4
+ * Returns true (addressed / no @ in play) when:
5
+ * - No `mentions` → true (backward-compat single-agent /ws/session; sensible
6
+ * default when a session has a single provider agent).
7
+ * - `mentions` present + this agent's participantId is in the list → true.
8
+ * Returns false only when `mentions` names others and our participantId is known
9
+ * but absent (or participantId unknown conservatively false).
11
10
  *
12
11
  * Note: in legacy /ws/session mode there is no top-level `mentions` and
13
- * `myParticipantId` is undefined → always wakes (unchanged behavior). */
14
- export declare function shouldWakeAgent(myParticipantId: string | undefined, message: Record<string, unknown>): boolean;
12
+ * `myParticipantId` is undefined → true (unchanged). */
13
+ export declare function isAddressedToMe(myParticipantId: string | undefined, message: Record<string, unknown>): boolean;
15
14
  /** Reduce a `model.usage` diagnostic `usage` object to a single token total.
16
15
  * Preference: `total` → `promptTokens + output` → `input + output`. 0 if none. */
17
16
  export declare function computeUsageTotal(usage: {
@@ -2,20 +2,19 @@
2
2
  //
3
3
  // Kept dependency-free + side-effect-free so the @-routing and token-usage logic
4
4
  // can be unit-tested without standing up an OpenClaw runtime or a live WS server.
5
- /** @-routing policy (spec §6.3 + Plan 4b-2.2).
5
+ /** Is this provider agent @-addressed by the inbound group message? (D1: was a
6
+ * wake/sleep GATE — now CONTEXT only; the agent self-decides whether to reply.)
6
7
  *
7
- * Decides whether THIS provider agent should wake (drive the OpenClaw bot) for an
8
- * inbound group message:
9
- * - No `mentions` wake. Backward-compatible with the single-agent /ws/session
10
- * plugin (where every consumer message is answered). Also the sensible default
11
- * when a session has a single provider agent.
12
- * - `mentions` present wake ONLY if this agent's participantId is in the list.
13
- * Non-@-ed agents stay silent (spec §6.3). If our participantId is unknown
14
- * (handshake not yet cached), we cannot match → sleep (safer than guessing).
8
+ * Returns true (addressed / no @ in play) when:
9
+ * - No `mentions` → true (backward-compat single-agent /ws/session; sensible
10
+ * default when a session has a single provider agent).
11
+ * - `mentions` present + this agent's participantId is in the list → true.
12
+ * Returns false only when `mentions` names others and our participantId is known
13
+ * but absent (or participantId unknown conservatively false).
15
14
  *
16
15
  * Note: in legacy /ws/session mode there is no top-level `mentions` and
17
- * `myParticipantId` is undefined → always wakes (unchanged behavior). */
18
- export function shouldWakeAgent(myParticipantId, message) {
16
+ * `myParticipantId` is undefined → true (unchanged). */
17
+ export function isAddressedToMe(myParticipantId, message) {
19
18
  const raw = message["mentions"];
20
19
  const mentions = Array.isArray(raw) ? raw : [];
21
20
  if (mentions.length === 0)
@@ -1,5 +1,7 @@
1
1
  export interface GuardrailResult {
2
2
  blocked: boolean;
3
+ /** D2: silent drop (no reply, no warning) — vs `blocked` which replies with a warning. */
4
+ drop?: boolean;
3
5
  reason?: string;
4
6
  }
5
7
  export declare function loadGuardrails(path?: string): string;
@@ -12,3 +14,19 @@ export declare function parseGuardrailRules(content: string): {
12
14
  reason: string;
13
15
  }[];
14
16
  export declare function checkGuardrails(text: string, fileContent?: string): GuardrailResult;
17
+ /**
18
+ * D2 prefilter: silent-drop obvious garbage (no reply, no warning) BEFORE driving the
19
+ * agent. Conservative — only drops content with no alphanumeric/CJK at all (pure
20
+ * symbols/punctuation/whitespace beyond the empty-trim check). Legit messages pass.
21
+ */
22
+ export declare function prefilter(text: string): GuardrailResult;
23
+ /**
24
+ * D3 phase-1 triage (default OFF, gated by `triageTwoPhase` config): cheap rule-based
25
+ * pre-screen BEFORE the full guardrail + dispatch. Returns {skip} to silent-drop what's
26
+ * clearly not worth a full turn. Conservative; twoPhase is off by default so this only
27
+ * runs when explicitly enabled.
28
+ */
29
+ export declare function phase1Triage(text: string): {
30
+ skip: boolean;
31
+ reason?: string;
32
+ };
@@ -52,3 +52,28 @@ export function checkGuardrails(text, fileContent) {
52
52
  }
53
53
  return { blocked: false };
54
54
  }
55
+ /**
56
+ * D2 prefilter: silent-drop obvious garbage (no reply, no warning) BEFORE driving the
57
+ * agent. Conservative — only drops content with no alphanumeric/CJK at all (pure
58
+ * symbols/punctuation/whitespace beyond the empty-trim check). Legit messages pass.
59
+ */
60
+ export function prefilter(text) {
61
+ if (!/[A-Za-z0-9一-鿿]/.test(text)) {
62
+ return { blocked: false, drop: true, reason: "content has no alphanumeric/CJK — likely noise" };
63
+ }
64
+ return { blocked: false, drop: false };
65
+ }
66
+ /**
67
+ * D3 phase-1 triage (default OFF, gated by `triageTwoPhase` config): cheap rule-based
68
+ * pre-screen BEFORE the full guardrail + dispatch. Returns {skip} to silent-drop what's
69
+ * clearly not worth a full turn. Conservative; twoPhase is off by default so this only
70
+ * runs when explicitly enabled.
71
+ */
72
+ export function phase1Triage(text) {
73
+ const pre = prefilter(text);
74
+ if (pre.drop)
75
+ return { skip: true, reason: pre.reason };
76
+ if (text.trim().length < 2)
77
+ return { skip: true, reason: "content too short for a turn" };
78
+ return { skip: false };
79
+ }
package/dist/index.js CHANGED
@@ -64,6 +64,9 @@ const entry = defineChannelPluginEntry({
64
64
  // fall back to legacy /ws/session (e.g. before the server auto-provisions the
65
65
  // provider-agent participant — Plan 4b Part A — else new sessions reject 4013).
66
66
  const useGroupChannel = config.useGroupChannel ?? true;
67
+ // D3: triage switches (optional; prefilter defaults on at the provider, twoPhase off).
68
+ const triagePrefilter = config.triagePrefilter;
69
+ const triageTwoPhase = config.triageTwoPhase;
67
70
  // --- resolve the agents to serve (multi-agent array, else single token) ---
68
71
  // multi: config.agents:[{agentId,token}] → one ProviderClient per agent.
69
72
  // single: config.token (+ optional config.agentId), with ~/.clawrent/config.json
@@ -141,6 +144,8 @@ const entry = defineChannelPluginEntry({
141
144
  },
142
145
  onLog: (m) => ctx.logger?.info?.(`[clawrent] ${m}`),
143
146
  useGroupChannel,
147
+ triagePrefilter,
148
+ triageTwoPhase,
144
149
  }).catch((e) => {
145
150
  ctx.logger?.error?.(`[clawrent] startProvider failed (agent=${agentKey}): ${String(e)}`);
146
151
  return null;
@@ -18,6 +18,9 @@ export interface StartProviderOptions {
18
18
  onLog?: (msg: string) => void;
19
19
  /** Plan 4b: opt into /ws/group (participant-scoped) instead of /ws/session. */
20
20
  useGroupChannel: boolean;
21
+ /** D3: triage switches. prefilter default on; twoPhase default off. */
22
+ triagePrefilter?: boolean;
23
+ triageTwoPhase?: boolean;
21
24
  }
22
25
  export interface ProviderHandle {
23
26
  stop: () => Promise<void> | void;
package/dist/provider.js CHANGED
@@ -16,8 +16,8 @@
16
16
  // - recordInboundSession ← openclaw/plugin-sdk/conversation-runtime
17
17
  // - dispatchReplyWithBufferedBlockDispatcher ← openclaw/plugin-sdk/reply-dispatch-runtime
18
18
  // - runChannelInboundEvent ← openclaw/plugin-sdk/channel-inbound
19
- import { checkGuardrails, loadGuardrails } from "./guardrails.js";
20
- import { shouldWakeAgent, computeUsageTotal, sessionIdFromKey } from "./group-routing.js";
19
+ import { checkGuardrails, loadGuardrails, prefilter, phase1Triage } from "./guardrails.js";
20
+ import { isAddressedToMe, computeUsageTotal, sessionIdFromKey } from "./group-routing.js";
21
21
  import { onDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
22
22
  const CHANNEL = "clawrent";
23
23
  /**
@@ -119,11 +119,24 @@ export async function startProvider(opts) {
119
119
  log(`skip empty content session=${sessionId}`);
120
120
  return;
121
121
  }
122
- // Plan 4b-2.2: @-routingonly wake when this agent is addressed (spec §6.3).
123
- // /ws/session mode (no mentions, no participantId) always wakes (unchanged).
124
- if (!shouldWakeAgent(session?.participantId, message)) {
125
- log(`skip not-@-ed session=${sessionId} (mentions do not include this agent)`);
126
- return;
122
+ // D1: @-gate removed agent self-decides whether to reply (prompt-based).
123
+ // @ is now CONTEXT (addressedToMe), threaded to the agent via UntrustedStructuredContext (D2).
124
+ const addressedToMe = isAddressedToMe(session?.participantId, message);
125
+ // D2 prefilter (D3: gated by triagePrefilter, default on) — silent-drop garbage.
126
+ if (opts.triagePrefilter !== false) {
127
+ const pre = prefilter(content);
128
+ if (pre.drop) {
129
+ log(`prefilter drop session=${sessionId}: ${pre.reason}`);
130
+ return;
131
+ }
132
+ }
133
+ // D3 phase-1 (default off via triageTwoPhase) — cheap pre-screen before full dispatch.
134
+ if (opts.triageTwoPhase) {
135
+ const p1 = phase1Triage(content);
136
+ if (p1.skip) {
137
+ log(`phase-1 skip session=${sessionId}: ${p1.reason}`);
138
+ return;
139
+ }
127
140
  }
128
141
  // 护栏:危险指令直接拦截,不驱动 agent。
129
142
  const guard = checkGuardrails(content);
@@ -142,15 +155,37 @@ export async function startProvider(opts) {
142
155
  // Windows 文件名不允许冒号, storePath 需做安全转义。
143
156
  const storePath = routeSessionKey.replace(/[:]/g, "_");
144
157
  const consumerId = session?.consumerUserId ?? "consumer";
158
+ // D2: UntrustedStructuredContext — sender / mentions / gist / @-addressing. With the
159
+ // @-gate gone (D1), this is what lets the agent self-decide whether + how to reply.
160
+ const mentions = Array.isArray(message.mentions) ? message.mentions : [];
161
+ const untrusted = {
162
+ addressedToMe,
163
+ sender: {
164
+ participantId: message.sender?.participantId,
165
+ side: message.sender?.side,
166
+ agentId: message.sender?.agentId,
167
+ consumerUserId: session?.consumerUserId,
168
+ },
169
+ mentions,
170
+ gist: session?.taskDescription,
171
+ participantId: session?.participantId,
172
+ };
173
+ // Inject the context as a preamble in the agent-facing text so the model literally
174
+ // sees it (also available structured on ctxPayload.UntrustedStructuredContext).
175
+ const textForAgentContent = `[被@: ${addressedToMe ? "是" : "否"}] ` +
176
+ `[发送方: ${untrusted.sender.side ?? "unknown"}] ` +
177
+ `[会话: ${untrusted.gist ?? ""}] ` +
178
+ `[mentions: ${mentions.length}]\n${content}`;
145
179
  const ctxPayload = {
146
180
  From: `${CHANNEL}:${consumerId}`,
147
181
  Body: content,
148
182
  RawBody: content,
149
- BodyForAgent: content,
183
+ BodyForAgent: textForAgentContent,
150
184
  channel: CHANNEL,
151
185
  SessionKey: routeSessionKey,
186
+ UntrustedStructuredContext: untrusted,
152
187
  };
153
- const raw = { sessionId, consumerId, content, message };
188
+ const raw = { sessionId, consumerId, content, message, textForAgent: textForAgentContent };
154
189
  try {
155
190
  await opts.deps.runChannelInboundEvent({
156
191
  channel: CHANNEL,
@@ -166,7 +201,7 @@ export async function startProvider(opts) {
166
201
  id: `${r.sessionId}:${r.message?.id ?? Date.now()}`,
167
202
  timestamp: Date.now(),
168
203
  rawText: r.content,
169
- textForAgent: r.content,
204
+ textForAgent: r.textForAgent ?? r.content,
170
205
  textForCommands: r.content,
171
206
  raw: r,
172
207
  }),
@@ -2,7 +2,7 @@
2
2
  "id": "clawrent",
3
3
  "name": "ClawRent Channel",
4
4
  "description": "OpenClaw channel plugin that turns ClawRent rental sessions into native OpenClaw conversations, so a local ClawRent provider agent can answer tenants autonomously with its own model and identity. / OpenClaw 频道插件:把 ClawRent 租赁会话桥接成 OpenClaw 原生对话,让本地 ClawRent provider 智能体用自有模型与身份自动应答租户。",
5
- "version": "0.4.0",
5
+ "version": "0.6.0",
6
6
  "channels": ["clawrent"],
7
7
  "activation": {
8
8
  "onStartup": false,
@@ -52,6 +52,16 @@
52
52
  "title": "Use /ws/group participant-scoped channel (Plan 4b)",
53
53
  "description": "true (default) = connect /ws/group (participant-scoped, @-routing, per_token usage). false = legacy /ws/session (fallback). Requires the ClawRent server to auto-provision the provider-agent participant (Plan 4b Part A) — else new sessions reject with 4013."
54
54
  },
55
+ "triagePrefilter": {
56
+ "type": "boolean",
57
+ "title": "Triage: prefilter silent-drop",
58
+ "description": "true (default) = silent-drop obvious garbage (no alphanumeric/CJK) before driving the agent. false = pass everything through."
59
+ },
60
+ "triageTwoPhase": {
61
+ "type": "boolean",
62
+ "title": "Triage: two-phase pre-screen",
63
+ "description": "false (default). true = run a cheap rule-based phase-1 pre-screen (skip noise / over-short content) before the full guardrail + dispatch."
64
+ },
55
65
  "agents": {
56
66
  "type": "array",
57
67
  "title": "Multiple provider agents (multi-agent mode)",
@@ -89,6 +99,14 @@
89
99
  "guardrailsFile": {
90
100
  "label": "Guardrails file",
91
101
  "help": "外置护栏策略文件(每行 `/regex/ || 原因`,# 开头为注释)。规则追加在内置护栏之后,实现策略外置。"
102
+ },
103
+ "triagePrefilter": {
104
+ "label": "Triage: prefilter (silent-drop)",
105
+ "help": "默认开。静默丢弃明显垃圾内容(无字母/数字/中文,纯符号/空白)。关闭则全部放行。"
106
+ },
107
+ "triageTwoPhase": {
108
+ "label": "Triage: two-phase pre-screen",
109
+ "help": "默认关。开启后在完整护栏+派发前加一道廉价规则预筛(跳过噪音/过短内容)。"
92
110
  }
93
111
  }
94
112
  }
@@ -129,6 +147,14 @@
129
147
  "type": "boolean",
130
148
  "title": "Use /ws/group participant-scoped channel (Plan 4b)"
131
149
  },
150
+ "triagePrefilter": {
151
+ "type": "boolean",
152
+ "title": "Triage: prefilter silent-drop"
153
+ },
154
+ "triageTwoPhase": {
155
+ "type": "boolean",
156
+ "title": "Triage: two-phase pre-screen"
157
+ },
132
158
  "agents": {
133
159
  "type": "array",
134
160
  "title": "Multiple provider agents (multi-agent mode)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawrent/openclaw-channel",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "OpenClaw channel plugin that turns ClawRent rental sessions into native OpenClaw conversations, so a local ClawRent provider agent can answer tenants autonomously with its own model and identity. / OpenClaw 频道插件:把 ClawRent 租赁会话桥接成 OpenClaw 原生对话,让本地 ClawRent provider 智能体用自有模型与身份自动应答租户。",
5
5
  "license": "ISC",
6
6
  "type": "module",