@botcord/daemon 0.1.1 → 0.2.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/daemon-config-map.js +4 -3
- package/dist/daemon.js +35 -1
- package/dist/gateway/channels/botcord.d.ts +11 -0
- package/dist/gateway/channels/botcord.js +86 -6
- package/dist/gateway/dispatcher.d.ts +8 -1
- package/dist/gateway/dispatcher.js +14 -0
- package/dist/gateway/gateway.d.ts +6 -1
- package/dist/gateway/gateway.js +1 -0
- package/dist/gateway/router.d.ts +8 -3
- package/dist/gateway/router.js +14 -4
- package/dist/gateway/types.d.ts +6 -0
- package/dist/loop-risk.d.ts +65 -0
- package/dist/loop-risk.js +286 -0
- package/dist/system-context.d.ts +6 -0
- package/dist/system-context.js +21 -2
- package/dist/turn-text.js +120 -3
- package/package.json +2 -2
- package/src/__tests__/loop-risk.test.ts +172 -0
- package/src/__tests__/system-context.test.ts +35 -0
- package/src/__tests__/turn-text.test.ts +143 -0
- package/src/daemon-config-map.ts +4 -3
- package/src/daemon.ts +42 -1
- package/src/gateway/__tests__/botcord-channel.test.ts +89 -0
- package/src/gateway/__tests__/dispatcher.test.ts +40 -0
- package/src/gateway/__tests__/router.test.ts +27 -1
- package/src/gateway/channels/botcord.ts +102 -6
- package/src/gateway/dispatcher.ts +20 -0
- package/src/gateway/gateway.ts +7 -0
- package/src/gateway/router.ts +18 -4
- package/src/gateway/types.ts +9 -0
- package/src/loop-risk.ts +322 -0
- package/src/system-context.ts +26 -2
- package/src/turn-text.ts +151 -3
package/src/turn-text.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* model the context it needs.
|
|
25
25
|
*/
|
|
26
26
|
import type { GatewayInboundMessage } from "./gateway/index.js";
|
|
27
|
-
import { sanitizeSenderName } from "./gateway/index.js";
|
|
27
|
+
import { sanitizeSenderName, sanitizeUntrustedContent } from "./gateway/index.js";
|
|
28
28
|
import { classifyActivitySender } from "./sender-classify.js";
|
|
29
29
|
|
|
30
30
|
const GROUP_HINT =
|
|
@@ -34,6 +34,68 @@ const DIRECT_HINT =
|
|
|
34
34
|
'[If the conversation has naturally concluded or no response is needed, ' +
|
|
35
35
|
'reply with exactly "NO_REPLY" and nothing else.]';
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Read the BotCord envelope type from a raw inbound message. Returns
|
|
39
|
+
* `undefined` when the message didn't come from the BotCord channel or the
|
|
40
|
+
* raw shape is unexpected — callers treat that the same as "message".
|
|
41
|
+
*/
|
|
42
|
+
function readEnvelopeType(raw: unknown): string | undefined {
|
|
43
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
44
|
+
const env = (raw as { envelope?: unknown }).envelope;
|
|
45
|
+
if (!env || typeof env !== "object") return undefined;
|
|
46
|
+
const t = (env as { type?: unknown }).type;
|
|
47
|
+
return typeof t === "string" ? t : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Minimal shape of one batched inbound entry. Matches the BotCord channel
|
|
51
|
+
* `BatchedInboxRaw.batch[]` elements but expressed structurally so the
|
|
52
|
+
* composer doesn't import channel internals. */
|
|
53
|
+
interface BatchedEntry {
|
|
54
|
+
hub_msg_id?: unknown;
|
|
55
|
+
text?: unknown;
|
|
56
|
+
envelope?: { from?: unknown; type?: unknown; payload?: { text?: unknown } };
|
|
57
|
+
source_type?: unknown;
|
|
58
|
+
source_user_name?: unknown;
|
|
59
|
+
mentioned?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read the `raw.batch` array emitted by the BotCord channel when inbox
|
|
64
|
+
* drain groups multiple messages for the same `(room, topic)`. Returns the
|
|
65
|
+
* list when present and well-shaped, else null. Single-message envelopes
|
|
66
|
+
* have no `batch` field and fall through to the single-message path.
|
|
67
|
+
*/
|
|
68
|
+
function readBatch(raw: unknown): BatchedEntry[] | null {
|
|
69
|
+
if (!raw || typeof raw !== "object") return null;
|
|
70
|
+
const b = (raw as { batch?: unknown }).batch;
|
|
71
|
+
if (!Array.isArray(b) || b.length < 2) return null;
|
|
72
|
+
return b as BatchedEntry[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function entryFromLabel(e: BatchedEntry): {
|
|
76
|
+
label: string;
|
|
77
|
+
kind: "human" | "agent";
|
|
78
|
+
envelopeType: string | undefined;
|
|
79
|
+
} {
|
|
80
|
+
const envType = typeof e.envelope?.type === "string" ? e.envelope.type : undefined;
|
|
81
|
+
const isHuman =
|
|
82
|
+
e.source_type === "dashboard_human_room" ||
|
|
83
|
+
(typeof e.envelope?.from === "string" && e.envelope.from.startsWith("hu_"));
|
|
84
|
+
const fromId = typeof e.envelope?.from === "string" ? e.envelope.from : "unknown";
|
|
85
|
+
const label = isHuman
|
|
86
|
+
? typeof e.source_user_name === "string" && e.source_user_name
|
|
87
|
+
? e.source_user_name
|
|
88
|
+
: "User"
|
|
89
|
+
: fromId;
|
|
90
|
+
return { label, kind: isHuman ? "human" : "agent", envelopeType: envType };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function entryText(e: BatchedEntry): string {
|
|
94
|
+
if (typeof e.text === "string") return e.text;
|
|
95
|
+
if (typeof e.envelope?.payload?.text === "string") return e.envelope.payload.text;
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
|
|
37
99
|
/**
|
|
38
100
|
* Compose the user-turn text for a BotCord inbound message.
|
|
39
101
|
*
|
|
@@ -54,6 +116,11 @@ export function composeBotCordUserTurn(msg: GatewayInboundMessage): string {
|
|
|
54
116
|
// system-context handles context; wrapping here would just add noise.
|
|
55
117
|
if (sender.kind === "owner") return trimmed;
|
|
56
118
|
|
|
119
|
+
const batch = readBatch(msg.raw);
|
|
120
|
+
if (batch) {
|
|
121
|
+
return composeBatchedTurn(msg, batch);
|
|
122
|
+
}
|
|
123
|
+
|
|
57
124
|
const conversation = msg.conversation;
|
|
58
125
|
const isGroup = conversation.kind === "group";
|
|
59
126
|
const roomTitle =
|
|
@@ -82,12 +149,93 @@ export function composeBotCordUserTurn(msg: GatewayInboundMessage): string {
|
|
|
82
149
|
|
|
83
150
|
const hint = isGroup ? GROUP_HINT : DIRECT_HINT;
|
|
84
151
|
|
|
85
|
-
|
|
152
|
+
// Contact-request envelopes travel through the same "inbound message"
|
|
153
|
+
// path as regular messages, but carry an additional expectation: the
|
|
154
|
+
// agent should surface the request to its owner rather than auto-accept
|
|
155
|
+
// or auto-reject. Mirrors `plugin/src/inbound.ts` §handleA2ASingle.
|
|
156
|
+
const isContactRequest = readEnvelopeType(msg.raw) === "contact_request";
|
|
157
|
+
const contactRequestHint = isContactRequest
|
|
158
|
+
? "[You received a contact request from " +
|
|
159
|
+
sanitizedSenderLabel +
|
|
160
|
+
". Use the botcord_notify tool to inform your owner about this request so " +
|
|
161
|
+
"they can decide whether to accept or reject it. Include the sender's " +
|
|
162
|
+
"agent ID and any message they attached.]"
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
const lines: string[] = [
|
|
86
166
|
headerFields.join(" | "),
|
|
87
167
|
`<${tag} sender="${sanitizedSenderLabel}" sender_kind="${senderKindAttr}">`,
|
|
88
168
|
trimmed,
|
|
89
169
|
`</${tag}>`,
|
|
90
170
|
"",
|
|
91
171
|
hint,
|
|
92
|
-
]
|
|
172
|
+
];
|
|
173
|
+
if (contactRequestHint) {
|
|
174
|
+
lines.push("", contactRequestHint);
|
|
175
|
+
}
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Render a batched turn (≥2 messages from the same room/topic folded into
|
|
181
|
+
* one envelope by `botcord.ts:normalizeInboxBatch`). Mirrors plugin's
|
|
182
|
+
* `handleA2AGroup` output shape so Claude Code sees the same prompt
|
|
183
|
+
* whether driven by OpenClaw or by daemon.
|
|
184
|
+
*/
|
|
185
|
+
function composeBatchedTurn(
|
|
186
|
+
msg: GatewayInboundMessage,
|
|
187
|
+
batch: BatchedEntry[],
|
|
188
|
+
): string {
|
|
189
|
+
const conversation = msg.conversation;
|
|
190
|
+
const isGroup = conversation.kind === "group";
|
|
191
|
+
const roomTitle =
|
|
192
|
+
typeof conversation.title === "string" ? conversation.title : undefined;
|
|
193
|
+
|
|
194
|
+
const header: string[] = [
|
|
195
|
+
`[BotCord Messages (${batch.length} new)]`,
|
|
196
|
+
`to: ${msg.accountId}`,
|
|
197
|
+
];
|
|
198
|
+
if (isGroup && roomTitle) {
|
|
199
|
+
const safeRoom = sanitizeSenderName(roomTitle.replace(/[\r\n]+/g, " "));
|
|
200
|
+
header.push(`room: ${safeRoom}`);
|
|
201
|
+
}
|
|
202
|
+
if (msg.mentioned) {
|
|
203
|
+
header.push("mentioned: true");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const blocks: string[] = [];
|
|
207
|
+
const contactRequestSenders: string[] = [];
|
|
208
|
+
for (const entry of batch) {
|
|
209
|
+
const { label, kind, envelopeType } = entryFromLabel(entry);
|
|
210
|
+
const safeLabel = sanitizeSenderName(label);
|
|
211
|
+
const raw = entryText(entry);
|
|
212
|
+
// Owner-trust bypass is handled at the outer level — by the time we
|
|
213
|
+
// reach a batched turn the sender classifier has already returned
|
|
214
|
+
// non-owner. Still sanitize defensively.
|
|
215
|
+
const safeBody = sanitizeUntrustedContent(raw);
|
|
216
|
+
const tag = kind === "human" ? "human-message" : "agent-message";
|
|
217
|
+
blocks.push(
|
|
218
|
+
`<${tag} sender="${safeLabel}" sender_kind="${kind}">\n${safeBody}\n</${tag}>`,
|
|
219
|
+
);
|
|
220
|
+
if (envelopeType === "contact_request") {
|
|
221
|
+
contactRequestSenders.push(safeLabel);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const hint = isGroup ? GROUP_HINT : DIRECT_HINT;
|
|
226
|
+
const lines: string[] = [header.join(" | "), blocks.join("\n"), "", hint];
|
|
227
|
+
|
|
228
|
+
if (contactRequestSenders.length > 0) {
|
|
229
|
+
// Dedup + list — multiple distinct senders show as "A, B".
|
|
230
|
+
const unique = Array.from(new Set(contactRequestSenders));
|
|
231
|
+
lines.push(
|
|
232
|
+
"",
|
|
233
|
+
"[You received a contact request from " +
|
|
234
|
+
unique.join(", ") +
|
|
235
|
+
". Use the botcord_notify tool to inform your owner about this request so " +
|
|
236
|
+
"they can decide whether to accept or reject it. Include the sender's " +
|
|
237
|
+
"agent ID and any message they attached.]",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return lines.join("\n");
|
|
93
241
|
}
|