@rubytech/taskmaster 1.0.63 → 1.0.65
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/pi-embedded-runner/compact.js +1 -1
- package/dist/agents/pi-embedded-runner/history.js +57 -15
- package/dist/agents/pi-embedded-runner/run/attempt.js +23 -5
- package/dist/agents/pi-embedded-runner/run.js +6 -31
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/system-prompt.js +20 -0
- package/dist/agents/taskmaster-tools.js +4 -0
- package/dist/agents/tool-policy.js +2 -0
- package/dist/agents/tools/message-history-tool.js +436 -0
- package/dist/agents/tools/sessions-history-tool.js +1 -0
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.js +10 -0
- package/dist/control-ui/assets/index-DmifehTc.css +1 -0
- package/dist/control-ui/assets/index-o5Xs9S4u.js +3166 -0
- package/dist/control-ui/assets/index-o5Xs9S4u.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/control-ui.js +173 -0
- package/dist/gateway/net.js +16 -0
- package/dist/gateway/protocol/client-info.js +1 -0
- package/dist/gateway/protocol/schema/logs-chat.js +3 -0
- package/dist/gateway/protocol/schema/sessions-transcript.js +1 -3
- package/dist/gateway/public-chat/deliver-otp.js +9 -0
- package/dist/gateway/public-chat/otp.js +60 -0
- package/dist/gateway/public-chat/session.js +45 -0
- package/dist/gateway/server/ws-connection/message-handler.js +17 -4
- package/dist/gateway/server-chat.js +22 -0
- package/dist/gateway/server-http.js +21 -3
- package/dist/gateway/server-methods/chat.js +38 -5
- package/dist/gateway/server-methods/public-chat.js +110 -0
- package/dist/gateway/server-methods/sessions-transcript.js +29 -46
- package/dist/gateway/server-methods.js +17 -0
- package/dist/hooks/bundled/conversation-archive/handler.js +23 -6
- package/dist/infra/session-recovery.js +1 -3
- package/dist/plugins/runtime/index.js +2 -0
- package/dist/utils/message-channel.js +3 -0
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +185 -5
- package/dist/control-ui/assets/index-BPvR6pln.js +0 -3021
- package/dist/control-ui/assets/index-BPvR6pln.js.map +0 -1
- package/dist/control-ui/assets/index-mweBpmCT.css +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC handlers for public chat: OTP verification and session resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { ErrorCodes, errorShape } from "../protocol/index.js";
|
|
6
|
+
import { requestOtp, verifyOtp } from "../public-chat/otp.js";
|
|
7
|
+
import { deliverOtp } from "../public-chat/deliver-otp.js";
|
|
8
|
+
import { buildPublicSessionKey, resolvePublicAgentId } from "../public-chat/session.js";
|
|
9
|
+
/** Minimal phone format check: starts with +, digits only, 7–15 digits. */
|
|
10
|
+
function isValidPhone(phone) {
|
|
11
|
+
return /^\+\d{7,15}$/.test(phone);
|
|
12
|
+
}
|
|
13
|
+
export const publicChatHandlers = {
|
|
14
|
+
/**
|
|
15
|
+
* Request an OTP code — sends a 6-digit code to the given phone via WhatsApp.
|
|
16
|
+
* Params: { phone: string }
|
|
17
|
+
*/
|
|
18
|
+
"public.otp.request": async ({ params, respond, context }) => {
|
|
19
|
+
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
20
|
+
if (!phone || !isValidPhone(phone)) {
|
|
21
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid phone number"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const cfg = loadConfig();
|
|
25
|
+
if (!cfg.publicChat?.enabled) {
|
|
26
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const result = requestOtp(phone);
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
respond(false, { retryAfterMs: result.retryAfterMs }, errorShape(ErrorCodes.INVALID_REQUEST, "rate limited — try again shortly"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await deliverOtp(phone, result.code);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
context.logGateway.warn(`public-chat OTP delivery failed: ${String(err)}`);
|
|
39
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "failed to send verification code"));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
respond(true, { ok: true });
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Verify an OTP code and return the session key.
|
|
46
|
+
* Params: { phone: string, code: string, name?: string }
|
|
47
|
+
*/
|
|
48
|
+
"public.otp.verify": async ({ params, respond }) => {
|
|
49
|
+
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
50
|
+
const code = typeof params.code === "string" ? params.code.trim() : "";
|
|
51
|
+
const name = typeof params.name === "string" ? params.name.trim() : undefined;
|
|
52
|
+
if (!phone || !isValidPhone(phone)) {
|
|
53
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid phone number"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!code) {
|
|
57
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "code required"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const cfg = loadConfig();
|
|
61
|
+
if (!cfg.publicChat?.enabled) {
|
|
62
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const result = verifyOtp(phone, code);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
const messages = {
|
|
68
|
+
not_found: "no pending verification for this number",
|
|
69
|
+
expired: "verification code expired",
|
|
70
|
+
max_attempts: "too many attempts — request a new code",
|
|
71
|
+
invalid: "incorrect code",
|
|
72
|
+
};
|
|
73
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, messages[result.error] ?? "verification failed"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const agentId = resolvePublicAgentId(cfg);
|
|
77
|
+
const sessionKey = buildPublicSessionKey(agentId, phone);
|
|
78
|
+
respond(true, {
|
|
79
|
+
ok: true,
|
|
80
|
+
sessionKey,
|
|
81
|
+
agentId,
|
|
82
|
+
phone,
|
|
83
|
+
name,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a session key for anonymous public chat.
|
|
88
|
+
* Params: { cookieId: string }
|
|
89
|
+
*/
|
|
90
|
+
"public.session": async ({ params, respond }) => {
|
|
91
|
+
const cookieId = typeof params.cookieId === "string" ? params.cookieId.trim() : "";
|
|
92
|
+
if (!cookieId || cookieId.length < 8 || cookieId.length > 128) {
|
|
93
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cookieId"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const cfg = loadConfig();
|
|
97
|
+
if (!cfg.publicChat?.enabled) {
|
|
98
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const agentId = resolvePublicAgentId(cfg);
|
|
102
|
+
const identifier = `anon-${cookieId}`;
|
|
103
|
+
const sessionKey = buildPublicSessionKey(agentId, identifier);
|
|
104
|
+
respond(true, {
|
|
105
|
+
ok: true,
|
|
106
|
+
sessionKey,
|
|
107
|
+
agentId,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { loadConfig } from "../../config/config.js";
|
|
3
3
|
import { ErrorCodes, errorShape, formatValidationErrors, validateSessionsTranscriptParams, } from "../protocol/index.js";
|
|
4
|
-
import { loadCombinedSessionStoreForGateway, resolveSessionTranscriptCandidates, } from "../session-utils.js";
|
|
4
|
+
import { listAgentsForGateway, loadCombinedSessionStoreForGateway, resolveSessionTranscriptCandidates, } from "../session-utils.js";
|
|
5
5
|
const DEFAULT_LIMIT = 200;
|
|
6
|
-
const DEFAULT_MAX_BYTES_PER_FILE = 128_000;
|
|
7
|
-
const CONTENT_MAX_CHARS = 2048;
|
|
8
|
-
const MAX_SESSIONS = 20;
|
|
9
|
-
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
10
6
|
function extractAgentId(sessionKey) {
|
|
11
7
|
// Keys look like "agent:public:dm:+447..." — second segment is the agent ID.
|
|
12
8
|
const parts = sessionKey.split(":");
|
|
13
9
|
return parts.length >= 2 ? parts[1] : "unknown";
|
|
14
10
|
}
|
|
15
|
-
function truncate(text, max) {
|
|
16
|
-
if (text.length <= max)
|
|
17
|
-
return text;
|
|
18
|
-
return text.slice(0, max);
|
|
19
|
-
}
|
|
20
11
|
function resolveTimestamp(entry, fileMtimeMs) {
|
|
21
12
|
if (typeof entry.timestamp === "string" && entry.timestamp)
|
|
22
13
|
return entry.timestamp;
|
|
@@ -39,7 +30,7 @@ function extractTextFromContentBlocks(blocks) {
|
|
|
39
30
|
}
|
|
40
31
|
return parts.join("\n");
|
|
41
32
|
}
|
|
42
|
-
function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs
|
|
33
|
+
function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs) {
|
|
43
34
|
const entries = [];
|
|
44
35
|
const ts = resolveTimestamp(line, fileMtimeMs);
|
|
45
36
|
const model = line.message?.model;
|
|
@@ -57,7 +48,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
57
48
|
agentId,
|
|
58
49
|
timestamp: ts,
|
|
59
50
|
type: "error",
|
|
60
|
-
content
|
|
51
|
+
content,
|
|
61
52
|
...(model ? { model } : {}),
|
|
62
53
|
});
|
|
63
54
|
return entries;
|
|
@@ -74,7 +65,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
74
65
|
agentId,
|
|
75
66
|
timestamp: ts,
|
|
76
67
|
type: "tool",
|
|
77
|
-
content
|
|
68
|
+
content,
|
|
78
69
|
...(line.toolName ? { toolName: line.toolName } : {}),
|
|
79
70
|
...(model ? { model } : {}),
|
|
80
71
|
});
|
|
@@ -93,7 +84,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
93
84
|
agentId,
|
|
94
85
|
timestamp: ts,
|
|
95
86
|
type: entryType,
|
|
96
|
-
content:
|
|
87
|
+
content: msg.content,
|
|
97
88
|
...(model ? { model } : {}),
|
|
98
89
|
});
|
|
99
90
|
return entries;
|
|
@@ -113,7 +104,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
113
104
|
agentId,
|
|
114
105
|
timestamp: ts,
|
|
115
106
|
type: "thinking",
|
|
116
|
-
content:
|
|
107
|
+
content: text,
|
|
117
108
|
...(model ? { model } : {}),
|
|
118
109
|
});
|
|
119
110
|
}
|
|
@@ -127,7 +118,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
127
118
|
agentId,
|
|
128
119
|
timestamp: ts,
|
|
129
120
|
type: "tool",
|
|
130
|
-
content
|
|
121
|
+
content,
|
|
131
122
|
...(toolName ? { toolName } : {}),
|
|
132
123
|
...(model ? { model } : {}),
|
|
133
124
|
});
|
|
@@ -143,7 +134,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
143
134
|
agentId,
|
|
144
135
|
timestamp: ts,
|
|
145
136
|
type: entryType,
|
|
146
|
-
content:
|
|
137
|
+
content: text,
|
|
147
138
|
...(model ? { model } : {}),
|
|
148
139
|
});
|
|
149
140
|
}
|
|
@@ -161,7 +152,7 @@ function expandLineToEntries(line, sessionId, sessionKey, agentId, fileMtimeMs,
|
|
|
161
152
|
agentId,
|
|
162
153
|
timestamp: ts,
|
|
163
154
|
type: entryType,
|
|
164
|
-
content:
|
|
155
|
+
content: text,
|
|
165
156
|
...(model ? { model } : {}),
|
|
166
157
|
});
|
|
167
158
|
}
|
|
@@ -215,46 +206,29 @@ export const sessionsTranscriptHandlers = {
|
|
|
215
206
|
}
|
|
216
207
|
const p = params;
|
|
217
208
|
const limit = p.limit ?? DEFAULT_LIMIT;
|
|
218
|
-
const maxBytesPerFile = p.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
|
|
219
209
|
const inputCursors = p.cursors ?? {};
|
|
220
|
-
const contentMaxChars = p.full ? Infinity : CONTENT_MAX_CHARS;
|
|
221
210
|
const agentFilter = p.agents && p.agents.length > 0 ? new Set(p.agents) : null;
|
|
222
211
|
try {
|
|
223
212
|
const cfg = loadConfig();
|
|
224
213
|
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg);
|
|
225
214
|
const storeKeys = Object.keys(store);
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
const cutoff = now - ACTIVE_WINDOW_MS;
|
|
229
|
-
const staleKeys = Object.entries(store)
|
|
230
|
-
.filter(([, entry]) => (entry.updatedAt ?? 0) < cutoff)
|
|
231
|
-
.map(([key, entry]) => `${key}(${entry.updatedAt ?? 0})`);
|
|
232
|
-
let activeSessions = Object.entries(store)
|
|
233
|
-
.filter(([, entry]) => {
|
|
234
|
-
const updatedAt = entry.updatedAt ?? 0;
|
|
235
|
-
return updatedAt >= cutoff;
|
|
236
|
-
})
|
|
237
|
-
.sort(([, a], [, b]) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
|
|
238
|
-
.slice(0, MAX_SESSIONS);
|
|
215
|
+
// All sessions sorted by recency — no time cutoff, no count cap.
|
|
216
|
+
let sessions = Object.entries(store).sort(([, a], [, b]) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
239
217
|
// Apply agent filter if provided
|
|
240
218
|
if (agentFilter) {
|
|
241
|
-
|
|
219
|
+
sessions = sessions.filter(([key]) => {
|
|
242
220
|
const agentId = extractAgentId(key);
|
|
243
221
|
return agentFilter.has(agentId);
|
|
244
222
|
});
|
|
245
223
|
}
|
|
246
|
-
|
|
247
|
-
context.logGateway.info(`sessions.transcript: storePath=${storePath} storeKeys=${storeKeys.length} active=${activeSessions.length} stale=${staleKeys.length} cutoffAge=24h`);
|
|
224
|
+
context.logGateway.info(`sessions.transcript: storePath=${storePath} storeKeys=${storeKeys.length} sessions=${sessions.length}`);
|
|
248
225
|
if (storeKeys.length === 0) {
|
|
249
226
|
context.logGateway.warn(`sessions.transcript: combined store is empty — no agents found or no session entries`);
|
|
250
227
|
}
|
|
251
|
-
else if (activeSessions.length === 0) {
|
|
252
|
-
context.logGateway.warn(`sessions.transcript: all ${storeKeys.length} sessions are stale (>24h): ${staleKeys.slice(0, 5).join(", ")}`);
|
|
253
|
-
}
|
|
254
228
|
const allEntries = [];
|
|
255
229
|
const cursors = {};
|
|
256
230
|
const agentIds = new Set();
|
|
257
|
-
for (const [sessionKey, sessionEntry] of
|
|
231
|
+
for (const [sessionKey, sessionEntry] of sessions) {
|
|
258
232
|
const sessionId = sessionEntry.sessionId;
|
|
259
233
|
if (!sessionId)
|
|
260
234
|
continue;
|
|
@@ -263,8 +237,7 @@ export const sessionsTranscriptHandlers = {
|
|
|
263
237
|
const sources = [{ sid: sessionId, file: sessionEntry.sessionFile }];
|
|
264
238
|
const prev = sessionEntry.previousSessions;
|
|
265
239
|
if (Array.isArray(prev)) {
|
|
266
|
-
|
|
267
|
-
for (const p of prev.slice(-3).reverse()) {
|
|
240
|
+
for (const p of prev.slice().reverse()) {
|
|
268
241
|
if (p.sessionId)
|
|
269
242
|
sources.push({ sid: p.sessionId, file: p.sessionFile });
|
|
270
243
|
}
|
|
@@ -294,7 +267,8 @@ export const sessionsTranscriptHandlers = {
|
|
|
294
267
|
result = await readTranscriptTail({
|
|
295
268
|
filePath,
|
|
296
269
|
cursor,
|
|
297
|
-
|
|
270
|
+
// Read entire file when no cursor — no byte cap
|
|
271
|
+
maxBytes: Infinity,
|
|
298
272
|
});
|
|
299
273
|
}
|
|
300
274
|
catch {
|
|
@@ -311,7 +285,7 @@ export const sessionsTranscriptHandlers = {
|
|
|
311
285
|
catch {
|
|
312
286
|
continue;
|
|
313
287
|
}
|
|
314
|
-
const expanded = expandLineToEntries(parsed, source.sid, sessionKey, agentId, result.fileMtimeMs
|
|
288
|
+
const expanded = expandLineToEntries(parsed, source.sid, sessionKey, agentId, result.fileMtimeMs);
|
|
315
289
|
allEntries.push(...expanded);
|
|
316
290
|
}
|
|
317
291
|
}
|
|
@@ -323,9 +297,18 @@ export const sessionsTranscriptHandlers = {
|
|
|
323
297
|
return tb - ta;
|
|
324
298
|
});
|
|
325
299
|
const entries = allEntries.slice(0, limit);
|
|
300
|
+
// Include configured agents so UI filter chips appear even when an
|
|
301
|
+
// agent has no sessions yet. When an agent filter is active (account
|
|
302
|
+
// scoping), only add config agents that match the filter — otherwise
|
|
303
|
+
// agents from other accounts leak into the chip list.
|
|
304
|
+
const { agents: configAgents } = listAgentsForGateway(cfg);
|
|
305
|
+
for (const a of configAgents) {
|
|
306
|
+
if (!agentFilter || agentFilter.has(a.id))
|
|
307
|
+
agentIds.add(a.id);
|
|
308
|
+
}
|
|
326
309
|
const agents = Array.from(agentIds).sort();
|
|
327
|
-
if (allEntries.length === 0 &&
|
|
328
|
-
context.logGateway.warn(`sessions.transcript: ${
|
|
310
|
+
if (allEntries.length === 0 && sessions.length > 0) {
|
|
311
|
+
context.logGateway.warn(`sessions.transcript: ${sessions.length} session(s) but 0 transcript entries — files may be missing or unreadable`);
|
|
329
312
|
}
|
|
330
313
|
respond(true, { entries, cursors, agents }, undefined);
|
|
331
314
|
}
|
|
@@ -32,6 +32,7 @@ import { usageHandlers } from "./server-methods/usage.js";
|
|
|
32
32
|
import { voicewakeHandlers } from "./server-methods/voicewake.js";
|
|
33
33
|
import { webHandlers } from "./server-methods/web.js";
|
|
34
34
|
import { wizardHandlers } from "./server-methods/wizard.js";
|
|
35
|
+
import { publicChatHandlers } from "./server-methods/public-chat.js";
|
|
35
36
|
import { workspacesHandlers } from "./server-methods/workspaces.js";
|
|
36
37
|
const ADMIN_SCOPE = "operator.admin";
|
|
37
38
|
const READ_SCOPE = "operator.read";
|
|
@@ -40,6 +41,15 @@ const APPROVALS_SCOPE = "operator.approvals";
|
|
|
40
41
|
const PAIRING_SCOPE = "operator.pairing";
|
|
41
42
|
const APPROVAL_METHODS = new Set(["exec.approval.request", "exec.approval.resolve"]);
|
|
42
43
|
const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
|
|
44
|
+
/** Methods accessible to public-chat role (no PIN, no gateway auth). */
|
|
45
|
+
const PUBLIC_ROLE_METHODS = new Set([
|
|
46
|
+
"chat.send",
|
|
47
|
+
"chat.history",
|
|
48
|
+
"chat.abort",
|
|
49
|
+
"public.otp.request",
|
|
50
|
+
"public.otp.verify",
|
|
51
|
+
"public.session",
|
|
52
|
+
]);
|
|
43
53
|
const PAIRING_METHODS = new Set([
|
|
44
54
|
"node.pair.request",
|
|
45
55
|
"node.pair.list",
|
|
@@ -115,6 +125,12 @@ function authorizeGatewayMethod(method, client) {
|
|
|
115
125
|
return null;
|
|
116
126
|
const role = client.connect.role ?? "operator";
|
|
117
127
|
const scopes = client.connect.scopes ?? [];
|
|
128
|
+
// Public role: only allow explicitly listed methods
|
|
129
|
+
if (role === "public") {
|
|
130
|
+
if (PUBLIC_ROLE_METHODS.has(method))
|
|
131
|
+
return null;
|
|
132
|
+
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized: public role cannot call ${method}`);
|
|
133
|
+
}
|
|
118
134
|
if (NODE_ROLE_METHODS.has(method)) {
|
|
119
135
|
if (role === "node")
|
|
120
136
|
return null;
|
|
@@ -218,6 +234,7 @@ export const coreGatewayHandlers = {
|
|
|
218
234
|
...memoryHandlers,
|
|
219
235
|
...recordsHandlers,
|
|
220
236
|
...workspacesHandlers,
|
|
237
|
+
...publicChatHandlers,
|
|
221
238
|
};
|
|
222
239
|
export async function handleGatewayRequest(opts) {
|
|
223
240
|
const { req, respond, client, isWebchatConnect, context } = opts;
|
|
@@ -65,11 +65,17 @@ function formatTime(timestamp) {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* Format date for section header
|
|
68
|
+
* Format date for section header.
|
|
69
|
+
* Uses local date (consistent with formatTime) — NOT UTC.
|
|
70
|
+
* Previously used toISOString() which is UTC, causing date/time mismatches
|
|
71
|
+
* near midnight when local timezone differs from UTC.
|
|
69
72
|
*/
|
|
70
73
|
function formatDate(timestamp) {
|
|
71
74
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
|
72
|
-
|
|
75
|
+
const year = date.getFullYear();
|
|
76
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
77
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
78
|
+
return `${year}-${month}-${day}`;
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
81
|
* Get monthly log filename
|
|
@@ -84,7 +90,7 @@ function getMonthlyLogFilename(timestamp) {
|
|
|
84
90
|
* Archive a message to the conversation log
|
|
85
91
|
*/
|
|
86
92
|
async function archiveMessage(params) {
|
|
87
|
-
const { workspaceDir, subdir, role, text, timestamp, fileHeader } = params;
|
|
93
|
+
const { workspaceDir, subdir, role, text, timestamp, fileHeader, channel } = params;
|
|
88
94
|
// Build path: memory/{subdir}/conversations/YYYY-MM.md
|
|
89
95
|
const conversationsDir = path.join(workspaceDir, "memory", subdir, "conversations");
|
|
90
96
|
await fs.mkdir(conversationsDir, { recursive: true });
|
|
@@ -121,7 +127,8 @@ async function archiveMessage(params) {
|
|
|
121
127
|
}
|
|
122
128
|
// Format the message - escape any markdown in the text
|
|
123
129
|
const cleanText = text?.trim() || "(no text)";
|
|
124
|
-
|
|
130
|
+
const channelTag = channel ? ` [${channel}]` : "";
|
|
131
|
+
entryParts.push(`### ${timeStr} — ${role}${channelTag}`);
|
|
125
132
|
entryParts.push(cleanText);
|
|
126
133
|
entryParts.push("");
|
|
127
134
|
const entry = entryParts.join("\n");
|
|
@@ -155,6 +162,8 @@ const archiveConversation = async (event) => {
|
|
|
155
162
|
}
|
|
156
163
|
// Get timestamp from context or event
|
|
157
164
|
const timestamp = context.timestamp ?? event.timestamp;
|
|
165
|
+
// Channel identifier from hook context (e.g. "whatsapp", "webchat", "imessage")
|
|
166
|
+
const channel = context.channel ?? undefined;
|
|
158
167
|
// Determine conversation type from session key and route to correct archive path
|
|
159
168
|
const peer = extractPeerFromSessionKey(event.sessionKey);
|
|
160
169
|
const groupId = peer ? null : extractGroupIdFromSessionKey(event.sessionKey);
|
|
@@ -165,7 +174,7 @@ const archiveConversation = async (event) => {
|
|
|
165
174
|
const subdir = isAdminAgent ? "admin" : `users/${peer}`;
|
|
166
175
|
const inboundRole = isAdminAgent ? "Admin" : "User";
|
|
167
176
|
const role = event.action === "inbound" ? inboundRole : "Assistant";
|
|
168
|
-
await archiveMessage({ workspaceDir, subdir, role, text, timestamp });
|
|
177
|
+
await archiveMessage({ workspaceDir, subdir, role, text, timestamp, channel });
|
|
169
178
|
}
|
|
170
179
|
else if (groupId) {
|
|
171
180
|
// Group message — archive under memory/groups/{groupId}/
|
|
@@ -193,12 +202,20 @@ const archiveConversation = async (event) => {
|
|
|
193
202
|
text,
|
|
194
203
|
timestamp,
|
|
195
204
|
fileHeader,
|
|
205
|
+
channel,
|
|
196
206
|
});
|
|
197
207
|
}
|
|
198
208
|
else if (isWebchat) {
|
|
199
209
|
// Webchat (control panel) — archive under memory/admin/conversations/
|
|
200
210
|
const role = event.action === "inbound" ? "Admin" : "Assistant";
|
|
201
|
-
await archiveMessage({
|
|
211
|
+
await archiveMessage({
|
|
212
|
+
workspaceDir,
|
|
213
|
+
subdir: "admin",
|
|
214
|
+
role,
|
|
215
|
+
text,
|
|
216
|
+
timestamp,
|
|
217
|
+
channel: channel ?? "webchat",
|
|
218
|
+
});
|
|
202
219
|
}
|
|
203
220
|
else {
|
|
204
221
|
// Unknown session key format — skip
|
|
@@ -171,9 +171,7 @@ export async function recoverOrphanedSessions(params) {
|
|
|
171
171
|
// List JSONL files on disk
|
|
172
172
|
let files;
|
|
173
173
|
try {
|
|
174
|
-
files = fs
|
|
175
|
-
.readdirSync(sessionsDir)
|
|
176
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
174
|
+
files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
177
175
|
}
|
|
178
176
|
catch {
|
|
179
177
|
continue;
|
|
@@ -11,6 +11,7 @@ import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-di
|
|
|
11
11
|
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
|
12
12
|
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
|
13
13
|
import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "../../agents/tools/memory-tool.js";
|
|
14
|
+
import { createMessageHistoryTool } from "../../agents/tools/message-history-tool.js";
|
|
14
15
|
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
|
15
16
|
import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js";
|
|
16
17
|
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
|
@@ -123,6 +124,7 @@ export function createPluginRuntime() {
|
|
|
123
124
|
createMemorySaveMediaTool,
|
|
124
125
|
createMemorySearchTool,
|
|
125
126
|
createMemoryWriteTool,
|
|
127
|
+
createMessageHistoryTool,
|
|
126
128
|
registerMemoryCli,
|
|
127
129
|
},
|
|
128
130
|
channel: {
|
|
@@ -25,6 +25,9 @@ export function isWebchatClient(client) {
|
|
|
25
25
|
return true;
|
|
26
26
|
return normalizeGatewayClientName(client?.id) === GATEWAY_CLIENT_NAMES.WEBCHAT_UI;
|
|
27
27
|
}
|
|
28
|
+
export function isPublicChatClient(client) {
|
|
29
|
+
return normalizeGatewayClientName(client?.id) === GATEWAY_CLIENT_NAMES.PUBLIC_CHAT;
|
|
30
|
+
}
|
|
28
31
|
export function normalizeMessageChannel(raw) {
|
|
29
32
|
const normalized = raw?.trim().toLowerCase();
|
|
30
33
|
if (!normalized)
|