@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.
Files changed (41) hide show
  1. package/dist/agents/pi-embedded-runner/compact.js +1 -1
  2. package/dist/agents/pi-embedded-runner/history.js +57 -15
  3. package/dist/agents/pi-embedded-runner/run/attempt.js +23 -5
  4. package/dist/agents/pi-embedded-runner/run.js +6 -31
  5. package/dist/agents/pi-embedded-runner.js +1 -1
  6. package/dist/agents/system-prompt.js +20 -0
  7. package/dist/agents/taskmaster-tools.js +4 -0
  8. package/dist/agents/tool-policy.js +2 -0
  9. package/dist/agents/tools/message-history-tool.js +436 -0
  10. package/dist/agents/tools/sessions-history-tool.js +1 -0
  11. package/dist/build-info.json +3 -3
  12. package/dist/config/zod-schema.js +10 -0
  13. package/dist/control-ui/assets/index-DmifehTc.css +1 -0
  14. package/dist/control-ui/assets/index-o5Xs9S4u.js +3166 -0
  15. package/dist/control-ui/assets/index-o5Xs9S4u.js.map +1 -0
  16. package/dist/control-ui/index.html +2 -2
  17. package/dist/gateway/config-reload.js +1 -0
  18. package/dist/gateway/control-ui.js +173 -0
  19. package/dist/gateway/net.js +16 -0
  20. package/dist/gateway/protocol/client-info.js +1 -0
  21. package/dist/gateway/protocol/schema/logs-chat.js +3 -0
  22. package/dist/gateway/protocol/schema/sessions-transcript.js +1 -3
  23. package/dist/gateway/public-chat/deliver-otp.js +9 -0
  24. package/dist/gateway/public-chat/otp.js +60 -0
  25. package/dist/gateway/public-chat/session.js +45 -0
  26. package/dist/gateway/server/ws-connection/message-handler.js +17 -4
  27. package/dist/gateway/server-chat.js +22 -0
  28. package/dist/gateway/server-http.js +21 -3
  29. package/dist/gateway/server-methods/chat.js +38 -5
  30. package/dist/gateway/server-methods/public-chat.js +110 -0
  31. package/dist/gateway/server-methods/sessions-transcript.js +29 -46
  32. package/dist/gateway/server-methods.js +17 -0
  33. package/dist/hooks/bundled/conversation-archive/handler.js +23 -6
  34. package/dist/infra/session-recovery.js +1 -3
  35. package/dist/plugins/runtime/index.js +2 -0
  36. package/dist/utils/message-channel.js +3 -0
  37. package/package.json +1 -1
  38. package/taskmaster-docs/USER-GUIDE.md +185 -5
  39. package/dist/control-ui/assets/index-BPvR6pln.js +0 -3021
  40. package/dist/control-ui/assets/index-BPvR6pln.js.map +0 -1
  41. 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, maxChars = CONTENT_MAX_CHARS) {
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: truncate(content, maxChars),
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: truncate(content, maxChars),
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: truncate(msg.content, maxChars),
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: truncate(text, maxChars),
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: truncate(content, maxChars),
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: truncate(text, maxChars),
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: truncate(text, maxChars),
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
- // Filter to sessions updated within last 24 hours
227
- const now = Date.now();
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
- activeSessions = activeSessions.filter(([key]) => {
219
+ sessions = sessions.filter(([key]) => {
242
220
  const agentId = extractAgentId(key);
243
221
  return agentFilter.has(agentId);
244
222
  });
245
223
  }
246
- // Diagnostic: log store resolution so we can trace "missing session logs" reports.
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 activeSessions) {
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
- // Include recent previous sessions (newest first, limit 3 to bound IO)
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
- maxBytes: maxBytesPerFile,
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, contentMaxChars);
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 && activeSessions.length > 0) {
328
- context.logGateway.warn(`sessions.transcript: ${activeSessions.length} active session(s) but 0 transcript entries — files may be missing or unreadable`);
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
- return date.toISOString().split("T")[0]; // YYYY-MM-DD
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
- entryParts.push(`### ${timeStr} ${role}`);
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({ workspaceDir, subdir: "admin", role, text, timestamp });
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"