@rubytech/taskmaster 1.16.3 → 1.17.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.
- package/dist/agents/tools/logs-read-tool.js +9 -0
- package/dist/agents/tools/memory-tool.js +1 -0
- package/dist/auto-reply/group-activation.js +2 -0
- package/dist/auto-reply/reply/commands-session.js +28 -11
- package/dist/build-info.json +3 -3
- package/dist/config/group-policy.js +16 -0
- package/dist/config/zod-schema.providers-whatsapp.js +2 -0
- package/dist/control-ui/assets/{index-Bd75cI7J.js → index-Beuhzjy_.js} +525 -492
- package/dist/control-ui/assets/index-Beuhzjy_.js.map +1 -0
- package/dist/control-ui/assets/index-XqRo9tNW.css +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/cron/preloaded.js +27 -23
- package/dist/gateway/protocol/index.js +7 -2
- package/dist/gateway/protocol/schema/logs-chat.js +6 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -0
- package/dist/gateway/protocol/schema/sessions-transcript.js +1 -0
- package/dist/gateway/protocol/schema/sessions.js +6 -1
- package/dist/gateway/protocol/schema/whatsapp.js +24 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/public-chat/session-token.js +52 -0
- package/dist/gateway/public-chat-api.js +40 -13
- package/dist/gateway/server-methods/logs.js +17 -1
- package/dist/gateway/server-methods/public-chat.js +5 -0
- package/dist/gateway/server-methods/sessions-transcript.js +30 -6
- package/dist/gateway/server-methods/whatsapp-conversations.js +387 -0
- package/dist/gateway/server-methods-list.js +6 -0
- package/dist/gateway/server-methods.js +7 -0
- package/dist/gateway/server.impl.js +3 -1
- package/dist/gateway/sessions-patch.js +1 -1
- package/dist/hooks/bundled/ride-dispatch/HOOK.md +7 -6
- package/dist/hooks/bundled/ride-dispatch/handler.js +75 -30
- package/dist/memory/manager.js +3 -3
- package/dist/tui/tui-command-handlers.js +1 -1
- package/dist/web/auto-reply/monitor/group-activation.js +12 -10
- package/dist/web/auto-reply/monitor/group-gating.js +23 -2
- package/dist/web/auto-reply/monitor/on-message.js +27 -5
- package/dist/web/auto-reply/monitor/process-message.js +64 -53
- package/dist/web/inbound/monitor.js +30 -0
- package/extensions/whatsapp/src/channel.ts +1 -1
- package/package.json +1 -1
- package/skills/log-review/SKILL.md +17 -4
- package/skills/log-review/references/review-protocol.md +4 -4
- package/taskmaster-docs/USER-GUIDE.md +14 -0
- package/dist/control-ui/assets/index-Bd75cI7J.js.map +0 -1
- package/dist/control-ui/assets/index-BkymP95Y.css +0 -1
|
@@ -95,6 +95,12 @@ const BASE_METHODS = [
|
|
|
95
95
|
"chat.history",
|
|
96
96
|
"chat.abort",
|
|
97
97
|
"chat.send",
|
|
98
|
+
// WhatsApp conversation browser
|
|
99
|
+
"whatsapp.conversations",
|
|
100
|
+
"whatsapp.messages",
|
|
101
|
+
"whatsapp.groupInfo",
|
|
102
|
+
"whatsapp.setActivation",
|
|
103
|
+
"whatsapp.sendMessage",
|
|
98
104
|
];
|
|
99
105
|
export function listGatewayMethods() {
|
|
100
106
|
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
|
|
@@ -40,6 +40,7 @@ import { wifiHandlers } from "./server-methods/wifi.js";
|
|
|
40
40
|
import { workspacesHandlers } from "./server-methods/workspaces.js";
|
|
41
41
|
import { brandHandlers } from "./server-methods/brand.js";
|
|
42
42
|
import { businessHandlers } from "./server-methods/business.js";
|
|
43
|
+
import { whatsappConversationsHandlers } from "./server-methods/whatsapp-conversations.js";
|
|
43
44
|
const ADMIN_SCOPE = "operator.admin";
|
|
44
45
|
const READ_SCOPE = "operator.read";
|
|
45
46
|
const WRITE_SCOPE = "operator.write";
|
|
@@ -109,6 +110,9 @@ const READ_METHODS = new Set([
|
|
|
109
110
|
"memory.audit",
|
|
110
111
|
"qr.generate",
|
|
111
112
|
"business.openingHours.get",
|
|
113
|
+
"whatsapp.conversations",
|
|
114
|
+
"whatsapp.messages",
|
|
115
|
+
"whatsapp.groupInfo",
|
|
112
116
|
]);
|
|
113
117
|
const WRITE_METHODS = new Set([
|
|
114
118
|
"send",
|
|
@@ -124,6 +128,8 @@ const WRITE_METHODS = new Set([
|
|
|
124
128
|
"node.invoke",
|
|
125
129
|
"chat.send",
|
|
126
130
|
"chat.abort",
|
|
131
|
+
"whatsapp.setActivation",
|
|
132
|
+
"whatsapp.sendMessage",
|
|
127
133
|
]);
|
|
128
134
|
function authorizeGatewayMethod(method, client) {
|
|
129
135
|
// Access methods bypass all scope checks — needed before PIN login
|
|
@@ -250,6 +256,7 @@ export const coreGatewayHandlers = {
|
|
|
250
256
|
...networkHandlers,
|
|
251
257
|
...tailscaleHandlers,
|
|
252
258
|
...wifiHandlers,
|
|
259
|
+
...whatsappConversationsHandlers,
|
|
253
260
|
};
|
|
254
261
|
export async function handleGatewayRequest(opts) {
|
|
255
262
|
const { req, respond, client, isWebchatConnect, context } = opts;
|
|
@@ -475,11 +475,13 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
475
475
|
if (!bundledSkillsDir)
|
|
476
476
|
return;
|
|
477
477
|
try {
|
|
478
|
+
const workspaceIds = Object.keys(cfgAtStart.workspaces ?? {});
|
|
479
|
+
const accountIds = workspaceIds.length > 0 ? workspaceIds : [DEFAULT_ACCOUNT_ID];
|
|
478
480
|
const seeded = await seedPreloadedCronJobs({
|
|
479
481
|
bundledSkillsDir,
|
|
480
482
|
trackerPath: DEFAULT_SEED_TRACKER_PATH,
|
|
481
483
|
cronService: cron,
|
|
482
|
-
|
|
484
|
+
accountIds,
|
|
483
485
|
});
|
|
484
486
|
if (seeded > 0) {
|
|
485
487
|
logCron.info(`cron: seeded ${seeded} preloaded job(s)`);
|
|
@@ -274,7 +274,7 @@ export async function applySessionsPatchToStore(params) {
|
|
|
274
274
|
else if (raw !== undefined) {
|
|
275
275
|
const normalized = normalizeGroupActivation(String(raw));
|
|
276
276
|
if (!normalized) {
|
|
277
|
-
return invalid('invalid groupActivation (use "mention"|"always")');
|
|
277
|
+
return invalid('invalid groupActivation (use "mention"|"always"|"off")');
|
|
278
278
|
}
|
|
279
279
|
next.groupActivation = normalized;
|
|
280
280
|
}
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
"taskmaster":
|
|
8
8
|
{
|
|
9
9
|
"emoji": "🚕",
|
|
10
|
-
"events": ["memory:add", "message:
|
|
10
|
+
"events": ["memory:add", "message:before-dispatch"],
|
|
11
11
|
"requires": { "config": ["workspace.dir"] },
|
|
12
12
|
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with Taskmaster" }],
|
|
13
13
|
},
|
|
@@ -28,13 +28,13 @@ When the public agent writes a file matching `shared/dispatch/{jobId}-{phase}.md
|
|
|
28
28
|
2. **Dispatches to admin agent** in a session scoped to the booking (`ride-{jobId}`)
|
|
29
29
|
3. **Admin agent autonomously processes** — contacts drivers, generates payment links, or finalises bookings
|
|
30
30
|
|
|
31
|
-
### Driver Replies (message:
|
|
31
|
+
### Driver Replies (message:before-dispatch)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Before a WhatsApp DM is dispatched to the public agent's LLM:
|
|
34
34
|
|
|
35
35
|
1. **Checks the active negotiation index** at `shared/active-negotiations/{phone}.md`
|
|
36
|
-
2. **If the driver has an active negotiation**, dispatches
|
|
37
|
-
3. **If no active negotiation**,
|
|
36
|
+
2. **If the driver has an active negotiation**, sets `event.suppress = true` and dispatches the reply to the admin's ride session — the public agent never sees the message
|
|
37
|
+
3. **If no active negotiation**, does nothing — the message proceeds normally to the public agent
|
|
38
38
|
|
|
39
39
|
## Why This Exists
|
|
40
40
|
|
|
@@ -44,7 +44,8 @@ The public agent must not have `contact_lookup` or `message` tools — exposing
|
|
|
44
44
|
|
|
45
45
|
- Only fires for **Beagle Zanzibar agents** (detected by `beagle` in agent ID)
|
|
46
46
|
- **Deduplicates** — same dispatch file within 30 seconds is ignored
|
|
47
|
-
- **
|
|
47
|
+
- **Driver suppression** — driver replies set `event.suppress = true` so the public agent's LLM is never invoked for driver messages
|
|
48
|
+
- **Non-blocking** — admin dispatch is fire-and-forget after suppression is set
|
|
48
49
|
- Admin agent uses `contact_lookup` → `message` → `memory_write` for driver outreach
|
|
49
50
|
- Admin agent uses `message` to inject results (offers, payment links, driver details) into tourist's conversation via cross-agent echo
|
|
50
51
|
|
|
@@ -86,25 +86,54 @@ function extractPeerFromSessionKey(sessionKey) {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Find the admin agent
|
|
90
|
-
*
|
|
89
|
+
* Find the sibling admin agent for a given public agent.
|
|
90
|
+
*
|
|
91
|
+
* Convention: if the public agent is `beagle-zanzibar-public`, the admin is
|
|
92
|
+
* `beagle-zanzibar-admin`. Falls back to any admin agent that shares the same
|
|
93
|
+
* Beagle prefix, then the global default.
|
|
91
94
|
*/
|
|
92
|
-
function
|
|
95
|
+
function findSiblingAdminAgentId(cfg, publicAgentId) {
|
|
93
96
|
const agents = cfg.agents?.list ?? [];
|
|
97
|
+
// Derive sibling: beagle-zanzibar-public → beagle-zanzibar-admin
|
|
98
|
+
if (publicAgentId.endsWith("-public")) {
|
|
99
|
+
const siblingId = publicAgentId.replace(/-public$/, "-admin");
|
|
100
|
+
if (agents.some((a) => a.id === siblingId))
|
|
101
|
+
return siblingId;
|
|
102
|
+
}
|
|
103
|
+
// Fallback: any admin agent sharing a Beagle prefix
|
|
104
|
+
const prefix = publicAgentId.replace(/-public$/, "");
|
|
105
|
+
const match = agents.find((a) => a.id?.startsWith(prefix) && a.id?.endsWith("-admin"));
|
|
106
|
+
if (match?.id)
|
|
107
|
+
return match.id;
|
|
108
|
+
// Last resort: global default
|
|
94
109
|
const admin = agents.find((a) => a.default === true);
|
|
95
110
|
if (admin?.id)
|
|
96
111
|
return admin.id;
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
return resolveDefaultAgentId(cfg) || null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Find the admin agent for a given account ID (used by payment webhooks
|
|
116
|
+
* where there's no public agent context). Convention: accountId
|
|
117
|
+
* "beagle-zanzibar" → admin agent "beagle-zanzibar-admin".
|
|
118
|
+
*/
|
|
119
|
+
function findAdminAgentForAccount(cfg, accountId) {
|
|
120
|
+
const agents = cfg.agents?.list ?? [];
|
|
121
|
+
const candidateId = `${accountId}-admin`;
|
|
122
|
+
if (agents.some((a) => a.id === candidateId))
|
|
123
|
+
return candidateId;
|
|
124
|
+
// Fallback: any admin agent whose ID starts with the account prefix
|
|
125
|
+
const match = agents.find((a) => a.id?.startsWith(accountId) && a.id?.endsWith("-admin"));
|
|
126
|
+
if (match?.id)
|
|
127
|
+
return match.id;
|
|
128
|
+
return resolveDefaultAgentId(cfg) || null;
|
|
99
129
|
}
|
|
100
130
|
/** Check if an agent ID belongs to a Beagle instance. */
|
|
101
131
|
function isBeagleAgent(agentId) {
|
|
102
132
|
return Boolean(agentId && agentId.toLowerCase().includes("beagle"));
|
|
103
133
|
}
|
|
104
|
-
/** Check if an agent is
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
return agent?.default !== true;
|
|
134
|
+
/** Check if an agent is a public (non-admin) agent by naming convention. */
|
|
135
|
+
function isPublicAgent(agentId) {
|
|
136
|
+
return agentId.endsWith("-public");
|
|
108
137
|
}
|
|
109
138
|
/**
|
|
110
139
|
* Read the active negotiation index file for a driver phone.
|
|
@@ -140,6 +169,7 @@ async function dispatchToAdmin(params) {
|
|
|
140
169
|
agentId: adminAgentId,
|
|
141
170
|
channel: "system",
|
|
142
171
|
peer: { kind: "dm", id: `ride-${jobId.toLowerCase()}` },
|
|
172
|
+
dmScope: "per-peer",
|
|
143
173
|
}).toLowerCase();
|
|
144
174
|
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
|
145
175
|
const envelope = formatInboundEnvelope({
|
|
@@ -189,7 +219,7 @@ async function dispatchToAdmin(params) {
|
|
|
189
219
|
// ---------------------------------------------------------------------------
|
|
190
220
|
// Build instruction text for each dispatch phase
|
|
191
221
|
// ---------------------------------------------------------------------------
|
|
192
|
-
function buildTripRequestInstruction(fields) {
|
|
222
|
+
function buildTripRequestInstruction(fields, resolvedAccountId) {
|
|
193
223
|
const jobId = fields.job_id ?? "UNKNOWN";
|
|
194
224
|
const touristPhone = fields.tourist_phone ?? "unknown";
|
|
195
225
|
const touristName = fields.tourist_name ?? "Tourist";
|
|
@@ -201,7 +231,7 @@ function buildTripRequestInstruction(fields) {
|
|
|
201
231
|
const luggage = fields.luggage ?? "none";
|
|
202
232
|
const specialRequests = fields.special_requests ?? "none";
|
|
203
233
|
const fareEstimate = fields.fare_estimate ?? "unknown";
|
|
204
|
-
const accountId =
|
|
234
|
+
const accountId = resolvedAccountId;
|
|
205
235
|
return (`[System: Ride Dispatch — Trip Request]\n\n` +
|
|
206
236
|
`A tourist has requested a ride. Process this by contacting available drivers.\n\n` +
|
|
207
237
|
`Job ID: ${jobId}\n` +
|
|
@@ -230,13 +260,13 @@ function buildTripRequestInstruction(fields) {
|
|
|
230
260
|
`9. Message the tourist with up to 3 competing offers: fare, driver rating, vehicle type, estimated journey time\n` +
|
|
231
261
|
` Do NOT reveal driver name, phone, or plate — those are gated by payment\n`);
|
|
232
262
|
}
|
|
233
|
-
function buildBookingConfirmInstruction(fields) {
|
|
263
|
+
function buildBookingConfirmInstruction(fields, resolvedAccountId) {
|
|
234
264
|
const jobId = fields.job_id ?? "UNKNOWN";
|
|
235
265
|
const touristPhone = fields.tourist_phone ?? "unknown";
|
|
236
266
|
const driverName = fields.driver_name ?? "unknown";
|
|
237
267
|
const driverPhone = fields.driver_phone ?? "unknown";
|
|
238
268
|
const fare = fields.fare ?? "unknown";
|
|
239
|
-
const accountId =
|
|
269
|
+
const accountId = resolvedAccountId;
|
|
240
270
|
return (`[System: Ride Dispatch — Booking Confirmation]\n\n` +
|
|
241
271
|
`The tourist has selected a driver. Generate a Stripe payment link and send it.\n\n` +
|
|
242
272
|
`Job ID: ${jobId}\n` +
|
|
@@ -290,8 +320,8 @@ async function handleMemoryAdd(event) {
|
|
|
290
320
|
return;
|
|
291
321
|
const jobId = match[1]; // e.g. BGL-0042
|
|
292
322
|
const phase = match[2]; // trip-request | booking-confirm
|
|
293
|
-
// Dedup
|
|
294
|
-
const dedupKey = `dispatch:${
|
|
323
|
+
// Dedup (use jobId+phase, not relativePath, since multiple watchers report different paths)
|
|
324
|
+
const dedupKey = `dispatch:${jobId}:${phase}`;
|
|
295
325
|
if (isDuplicate(dedupKey))
|
|
296
326
|
return;
|
|
297
327
|
// Read the dispatch file
|
|
@@ -317,18 +347,21 @@ async function handleMemoryAdd(event) {
|
|
|
317
347
|
console.warn("[ride-dispatch] Could not read config");
|
|
318
348
|
return;
|
|
319
349
|
}
|
|
320
|
-
const adminAgentId =
|
|
350
|
+
const adminAgentId = findSiblingAdminAgentId(cfg, agentId);
|
|
321
351
|
if (!adminAgentId) {
|
|
322
|
-
console.warn("[ride-dispatch] No admin agent found
|
|
352
|
+
console.warn("[ride-dispatch] No admin agent found for public agent:", agentId);
|
|
323
353
|
return;
|
|
324
354
|
}
|
|
325
|
-
|
|
355
|
+
// Resolve account: binding first, then derive from agent ID prefix
|
|
356
|
+
// (beagle-zanzibar-public → beagle-zanzibar), which matches WhatsApp account naming.
|
|
357
|
+
const accountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ??
|
|
358
|
+
(agentId.replace(/-(public|admin)$/, "") || "default");
|
|
326
359
|
let instruction;
|
|
327
360
|
if (phase === "trip-request") {
|
|
328
|
-
instruction = buildTripRequestInstruction(fields);
|
|
361
|
+
instruction = buildTripRequestInstruction(fields, accountId);
|
|
329
362
|
}
|
|
330
363
|
else if (phase === "booking-confirm") {
|
|
331
|
-
instruction = buildBookingConfirmInstruction(fields);
|
|
364
|
+
instruction = buildBookingConfirmInstruction(fields, accountId);
|
|
332
365
|
}
|
|
333
366
|
else {
|
|
334
367
|
return;
|
|
@@ -347,7 +380,13 @@ async function handleMemoryAdd(event) {
|
|
|
347
380
|
});
|
|
348
381
|
}
|
|
349
382
|
/**
|
|
350
|
-
* Handle message:
|
|
383
|
+
* Handle message:before-dispatch events — intercept driver replies before they
|
|
384
|
+
* reach the public agent's LLM.
|
|
385
|
+
*
|
|
386
|
+
* Sets event.suppress = true when the sender is a driver in active negotiation,
|
|
387
|
+
* then dispatches the reply to the admin agent's ride session instead.
|
|
388
|
+
* Because this handler is awaited by the caller, suppress takes effect before
|
|
389
|
+
* processForRoute is invoked.
|
|
351
390
|
*/
|
|
352
391
|
async function handleDriverReply(event) {
|
|
353
392
|
const context = event.context || {};
|
|
@@ -359,7 +398,7 @@ async function handleDriverReply(event) {
|
|
|
359
398
|
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
|
|
360
399
|
if (!agentId || !isBeagleAgent(agentId))
|
|
361
400
|
return;
|
|
362
|
-
if (!
|
|
401
|
+
if (!isPublicAgent(agentId))
|
|
363
402
|
return;
|
|
364
403
|
// Extract sender phone from session key
|
|
365
404
|
const senderPhone = extractPeerFromSessionKey(event.sessionKey);
|
|
@@ -367,18 +406,22 @@ async function handleDriverReply(event) {
|
|
|
367
406
|
return;
|
|
368
407
|
// Resolve workspace dir from the agent config to find active-negotiations
|
|
369
408
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
370
|
-
// Check active negotiation index
|
|
409
|
+
// Check active negotiation index (synchronous — must complete before we return
|
|
410
|
+
// so that event.suppress is set before the caller checks it)
|
|
371
411
|
const jobId = readActiveNegotiation(workspaceDir, senderPhone);
|
|
372
412
|
if (!jobId)
|
|
373
|
-
return; // Not a driver in active negotiation
|
|
413
|
+
return; // Not a driver in active negotiation — let public agent handle it
|
|
414
|
+
// Suppress delivery to the public agent before doing anything else
|
|
415
|
+
event.suppress = true;
|
|
374
416
|
// Dedup
|
|
375
417
|
const dedupKey = `reply:${senderPhone}:${Date.now().toString().slice(0, -4)}`; // ~10s granularity
|
|
376
418
|
if (isDuplicate(dedupKey))
|
|
377
419
|
return;
|
|
378
|
-
const adminAgentId =
|
|
420
|
+
const adminAgentId = findSiblingAdminAgentId(cfg, agentId);
|
|
379
421
|
if (!adminAgentId)
|
|
380
422
|
return;
|
|
381
|
-
const accountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ??
|
|
423
|
+
const accountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ??
|
|
424
|
+
(agentId.replace(/-(public|admin)$/, "") || "default");
|
|
382
425
|
const instruction = `[System: Ride Dispatch — Driver Reply]\n\n` +
|
|
383
426
|
`A driver has replied regarding job ${jobId}.\n\n` +
|
|
384
427
|
`Driver phone: ${senderPhone}\n` +
|
|
@@ -387,7 +430,7 @@ async function handleDriverReply(event) {
|
|
|
387
430
|
`If this is a fare quote, note it and compile offers when ready.\n` +
|
|
388
431
|
`If the driver is declining, update their status and active negotiation index.\n`;
|
|
389
432
|
console.log(`[ride-dispatch] Driver reply from ${senderPhone} for ${jobId}, dispatching to admin agent "${adminAgentId}"`);
|
|
390
|
-
// Fire and forget
|
|
433
|
+
// Fire and forget — suppress is already set, caller will skip processForRoute
|
|
391
434
|
dispatchToAdmin({
|
|
392
435
|
cfg,
|
|
393
436
|
adminAgentId,
|
|
@@ -420,9 +463,11 @@ export async function dispatchPaymentConfirmed(params) {
|
|
|
420
463
|
console.warn("[ride-dispatch] Could not read config for payment dispatch");
|
|
421
464
|
return;
|
|
422
465
|
}
|
|
423
|
-
|
|
466
|
+
// Payment webhooks don't have a public agent context. Derive admin from
|
|
467
|
+
// the accountId (e.g. "beagle-zanzibar" → "beagle-zanzibar-admin").
|
|
468
|
+
const adminAgentId = findAdminAgentForAccount(cfg, accountId);
|
|
424
469
|
if (!adminAgentId) {
|
|
425
|
-
console.warn("[ride-dispatch] No admin agent for payment dispatch");
|
|
470
|
+
console.warn("[ride-dispatch] No admin agent for payment dispatch, accountId:", accountId);
|
|
426
471
|
return;
|
|
427
472
|
}
|
|
428
473
|
const instruction = buildPaymentConfirmedInstruction({ bookingId, touristPhone, accountId });
|
|
@@ -443,7 +488,7 @@ const handleRideDispatch = async (event) => {
|
|
|
443
488
|
if (event.type === "memory" && event.action === "add") {
|
|
444
489
|
await handleMemoryAdd(event);
|
|
445
490
|
}
|
|
446
|
-
else if (event.type === "message" && event.action === "
|
|
491
|
+
else if (event.type === "message" && event.action === "before-dispatch") {
|
|
447
492
|
await handleDriverReply(event);
|
|
448
493
|
}
|
|
449
494
|
};
|
package/dist/memory/manager.js
CHANGED
|
@@ -538,7 +538,7 @@ export class MemoryIndexManager {
|
|
|
538
538
|
return this.syncing;
|
|
539
539
|
}
|
|
540
540
|
async readFile(params) {
|
|
541
|
-
const relPath =
|
|
541
|
+
const relPath = normalizeGroupIdInMemoryPath(normalizePhoneInMemoryPath(ensureMemoryPrefix(normalizeRelPath(params.relPath))));
|
|
542
542
|
if (!relPath || !isMemoryPath(relPath)) {
|
|
543
543
|
throw new Error(relPath
|
|
544
544
|
? `invalid path "${relPath}" — must start with "memory/" (e.g. "memory/admin/file.md")`
|
|
@@ -578,7 +578,7 @@ export class MemoryIndexManager {
|
|
|
578
578
|
* matching the session's scope configuration.
|
|
579
579
|
*/
|
|
580
580
|
async writeFile(params) {
|
|
581
|
-
const relPath =
|
|
581
|
+
const relPath = normalizeGroupIdInMemoryPath(normalizePhoneInMemoryPath(ensureMemoryPrefix(normalizeRelPath(params.relPath))));
|
|
582
582
|
if (!relPath || !isMemoryPath(relPath)) {
|
|
583
583
|
throw new Error(relPath
|
|
584
584
|
? `invalid path "${relPath}" — must start with "memory/" (e.g. "memory/admin/file.md")`
|
|
@@ -626,7 +626,7 @@ export class MemoryIndexManager {
|
|
|
626
626
|
let relPath;
|
|
627
627
|
if (params.destFolder) {
|
|
628
628
|
// Explicit folder — use as-is (scope checking enforces access)
|
|
629
|
-
relPath = normalizeGroupIdInMemoryPath(normalizePhoneInMemoryPath(`${params.destFolder}/${params.destFilename}`));
|
|
629
|
+
relPath = normalizeGroupIdInMemoryPath(normalizePhoneInMemoryPath(ensureMemoryPrefix(`${params.destFolder}/${params.destFilename}`)));
|
|
630
630
|
}
|
|
631
631
|
else {
|
|
632
632
|
// Default: memory/users/{peer}/media/{filename}
|
|
@@ -363,7 +363,7 @@ export function createCommandHandlers(context) {
|
|
|
363
363
|
try {
|
|
364
364
|
await client.patchSession({
|
|
365
365
|
key: state.currentSessionKey,
|
|
366
|
-
groupActivation: args === "always" ? "always" : "mention",
|
|
366
|
+
groupActivation: args === "always" ? "always" : args === "off" ? "off" : "mention",
|
|
367
367
|
});
|
|
368
368
|
chatLog.addSystem(`activation set to ${args}`);
|
|
369
369
|
await refreshSessionInfo();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadSessionStore, resolveGroupSessionKey, resolveStorePath, } from "../../../config/sessions.js";
|
|
1
|
+
import { resolveChannelGroupActivation, resolveChannelGroupPolicy, resolveChannelGroupRequireMention, } from "../../../config/group-policy.js";
|
|
2
|
+
import { resolveGroupSessionKey } from "../../../config/sessions.js";
|
|
4
3
|
export function resolveGroupPolicyFor(cfg, conversationId) {
|
|
5
4
|
const groupId = resolveGroupSessionKey({
|
|
6
5
|
From: conversationId,
|
|
@@ -26,12 +25,15 @@ export function resolveGroupRequireMentionFor(cfg, conversationId) {
|
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
export function resolveGroupActivationFor(params) {
|
|
29
|
-
const
|
|
30
|
-
|
|
28
|
+
const groupId = resolveGroupSessionKey({
|
|
29
|
+
From: params.conversationId,
|
|
30
|
+
ChatType: "group",
|
|
31
|
+
Provider: "whatsapp",
|
|
32
|
+
})?.id;
|
|
33
|
+
const configActivation = resolveChannelGroupActivation({
|
|
34
|
+
cfg: params.cfg,
|
|
35
|
+
channel: "whatsapp",
|
|
36
|
+
groupId: groupId ?? params.conversationId,
|
|
31
37
|
});
|
|
32
|
-
|
|
33
|
-
const entry = store[params.sessionKey];
|
|
34
|
-
const requireMention = resolveGroupRequireMentionFor(params.cfg, params.conversationId);
|
|
35
|
-
const defaultActivation = requireMention === false ? "always" : "mention";
|
|
36
|
-
return normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation;
|
|
38
|
+
return configActivation ?? "mention";
|
|
37
39
|
}
|
|
@@ -58,6 +58,10 @@ export function applyGroupGating(params) {
|
|
|
58
58
|
sessionKey: params.sessionKey,
|
|
59
59
|
conversationId: params.conversationId,
|
|
60
60
|
});
|
|
61
|
+
if (activation === "off") {
|
|
62
|
+
params.logVerbose(`Group ${params.conversationId} activation=off, ignoring`);
|
|
63
|
+
return { shouldProcess: false };
|
|
64
|
+
}
|
|
61
65
|
const requireMention = activation !== "always";
|
|
62
66
|
const selfJid = params.msg.selfJid?.replace(/:\\d+/, "");
|
|
63
67
|
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, "");
|
|
@@ -76,8 +80,25 @@ export function applyGroupGating(params) {
|
|
|
76
80
|
});
|
|
77
81
|
params.msg.wasMentioned = mentionGate.effectiveWasMentioned;
|
|
78
82
|
if (!shouldBypassMention && requireMention && mentionGate.shouldSkip) {
|
|
79
|
-
params.logVerbose(`Group message (no mention) —
|
|
80
|
-
|
|
83
|
+
params.logVerbose(`Group message (no mention) — accumulating context only in ${params.conversationId}: ${params.msg.body}`);
|
|
84
|
+
// Record the un-mentioned message in the group history buffer so it is
|
|
85
|
+
// available as context when the bot is eventually @mentioned.
|
|
86
|
+
const sender = params.msg.senderName && params.msg.senderE164
|
|
87
|
+
? `${params.msg.senderName} (${params.msg.senderE164})`
|
|
88
|
+
: (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown");
|
|
89
|
+
recordPendingHistoryEntryIfEnabled({
|
|
90
|
+
historyMap: params.groupHistories,
|
|
91
|
+
historyKey: params.groupHistoryKey,
|
|
92
|
+
limit: params.groupHistoryLimit,
|
|
93
|
+
entry: {
|
|
94
|
+
sender,
|
|
95
|
+
body: params.msg.body,
|
|
96
|
+
timestamp: params.msg.timestamp,
|
|
97
|
+
id: params.msg.id,
|
|
98
|
+
senderJid: params.msg.senderJid,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return { shouldProcess: true, contextOnly: true };
|
|
81
102
|
}
|
|
82
103
|
return { shouldProcess: true };
|
|
83
104
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadConfig } from "../../../config/config.js";
|
|
1
2
|
import { logVerbose } from "../../../globals.js";
|
|
2
3
|
import { isLicensed } from "../../../license/state.js";
|
|
3
4
|
import { createInternalHookEvent, triggerInternalHook } from "../../../hooks/internal-hooks.js";
|
|
@@ -29,7 +30,7 @@ export function createWebOnMessageHandler(params) {
|
|
|
29
30
|
buildCombinedEchoKey: params.echoTracker.buildCombinedKey,
|
|
30
31
|
groupHistory: opts?.groupHistory,
|
|
31
32
|
suppressGroupHistoryClear: opts?.suppressGroupHistoryClear,
|
|
32
|
-
|
|
33
|
+
contextOnly: opts?.contextOnly,
|
|
33
34
|
});
|
|
34
35
|
return async (msg) => {
|
|
35
36
|
// Block all message processing when unlicensed.
|
|
@@ -40,8 +41,12 @@ export function createWebOnMessageHandler(params) {
|
|
|
40
41
|
logVerbose(`[on-message] processing message from=${msg.from} body=${msg.body?.slice(0, 60)}`);
|
|
41
42
|
const conversationId = msg.conversationId ?? msg.from;
|
|
42
43
|
const peerId = resolvePeerId(msg);
|
|
44
|
+
// Read config fresh for routing so that binding changes (e.g. admin revocation)
|
|
45
|
+
// take effect on the next message without requiring a channel restart.
|
|
46
|
+
// `loadConfig()` has a 200ms cache so this is cheap under normal message rates.
|
|
47
|
+
const routingCfg = loadConfig();
|
|
43
48
|
const route = resolveAgentRoute({
|
|
44
|
-
cfg:
|
|
49
|
+
cfg: routingCfg,
|
|
45
50
|
channel: "whatsapp",
|
|
46
51
|
accountId: msg.accountId,
|
|
47
52
|
peer: {
|
|
@@ -67,7 +72,7 @@ export function createWebOnMessageHandler(params) {
|
|
|
67
72
|
params.echoTracker.forget(msg.body);
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
let
|
|
75
|
+
let contextOnly = false;
|
|
71
76
|
if (msg.chatType === "group") {
|
|
72
77
|
const metaCtx = {
|
|
73
78
|
From: msg.from,
|
|
@@ -132,7 +137,7 @@ export function createWebOnMessageHandler(params) {
|
|
|
132
137
|
}
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
|
-
|
|
140
|
+
contextOnly = gating.contextOnly ?? false;
|
|
136
141
|
}
|
|
137
142
|
else {
|
|
138
143
|
// Ensure `peerId` for DMs is stable and stored as E.164 when possible.
|
|
@@ -153,8 +158,25 @@ export function createWebOnMessageHandler(params) {
|
|
|
153
158
|
})) {
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
161
|
+
// Fire message:before-dispatch awaited so hooks can suppress processing.
|
|
162
|
+
// Unlike message:inbound (fire-and-forget inside processMessage), this is
|
|
163
|
+
// awaited here, allowing a handler to set event.suppress = true before
|
|
164
|
+
// the public agent's LLM is invoked. Used by ride-dispatch to intercept
|
|
165
|
+
// driver replies and route them to the admin agent instead.
|
|
166
|
+
const beforeDispatch = createInternalHookEvent("message", "before-dispatch", route.sessionKey, {
|
|
167
|
+
from: msg.senderE164 ?? msg.from,
|
|
168
|
+
to: msg.to,
|
|
169
|
+
text: msg.body,
|
|
170
|
+
chatType: msg.chatType,
|
|
171
|
+
agentId: route.agentId,
|
|
172
|
+
channel: "whatsapp",
|
|
173
|
+
cfg: params.cfg,
|
|
174
|
+
});
|
|
175
|
+
await triggerInternalHook(beforeDispatch);
|
|
176
|
+
if (beforeDispatch.suppress)
|
|
177
|
+
return;
|
|
156
178
|
await processForRoute(msg, route, groupHistoryKey, {
|
|
157
|
-
|
|
179
|
+
contextOnly: contextOnly || undefined,
|
|
158
180
|
});
|
|
159
181
|
};
|
|
160
182
|
}
|