@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,436 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../agent-scope.js";
5
+ import { jsonResult, readNumberParam, readStringParam } from "./common.js";
6
+ /**
7
+ * Schema for the message_history tool.
8
+ *
9
+ * - source: Identifies whose conversation to retrieve.
10
+ * "current" (default) resolves from the agent's own session key.
11
+ * "admin" reads admin/webchat conversations.
12
+ * A phone number (e.g. "+447857934268") reads that user's DM archive.
13
+ * A group ID reads group conversation archive.
14
+ * "discover" lists available conversation sources.
15
+ * "all" retrieves messages across all accessible conversations.
16
+ *
17
+ * - limit: Number of messages to return (default 30, max 500).
18
+ *
19
+ * - date: Optional YYYY-MM-DD filter. Without it, returns the most recent messages.
20
+ * - search: Case-insensitive substring filter on message text.
21
+ * - after/before: Time-range filter (HH:MM) within a date.
22
+ * - channel: Filter by channel (e.g. "whatsapp", "webchat", "imessage").
23
+ */
24
+ const MessageHistorySchema = Type.Object({
25
+ source: Type.Optional(Type.String({
26
+ description: 'Whose conversation to retrieve: "current" (this session, default), "admin" (admin/webchat), ' +
27
+ 'a phone number (e.g. "+447857934268") for a WhatsApp DM, a group JID for group chat, ' +
28
+ '"discover" to list available conversations, or "all" for messages across all conversations.',
29
+ })),
30
+ limit: Type.Optional(Type.Number({
31
+ description: "Number of messages to return (default 30, max 500). Returns the most recent.",
32
+ minimum: 1,
33
+ maximum: 500,
34
+ })),
35
+ date: Type.Optional(Type.String({
36
+ description: "Filter to a specific date (YYYY-MM-DD). Without this, returns the most recent messages across all dates.",
37
+ })),
38
+ search: Type.Optional(Type.String({
39
+ description: "Case-insensitive text search. Only messages containing this substring are returned.",
40
+ })),
41
+ after: Type.Optional(Type.String({
42
+ description: 'Include only messages at or after this time (HH:MM, 24h format). Requires "date" to be set.',
43
+ })),
44
+ before: Type.Optional(Type.String({
45
+ description: 'Include only messages before this time (HH:MM, 24h format). Requires "date" to be set.',
46
+ })),
47
+ channel: Type.Optional(Type.String({
48
+ description: 'Filter messages by channel (e.g. "whatsapp", "webchat", "imessage"). Only works for archives written after channel tagging was enabled.',
49
+ })),
50
+ });
51
+ const DATE_HEADER = /^## (\d{4}-\d{2}-\d{2})\s*$/;
52
+ // Captures: time, role, optional [channel] tag at end of line
53
+ const MESSAGE_HEADER = /^### (\d{2}:\d{2}) — (.+?)(?:\s+\[(\w+)\])?\s*$/;
54
+ /**
55
+ * Parse a conversation archive markdown file into structured messages.
56
+ * The archive format is:
57
+ *
58
+ * ```
59
+ * ## 2026-02-18
60
+ *
61
+ * ### 09:43 — Admin
62
+ * Message text here.
63
+ *
64
+ * ### 09:44 — Assistant
65
+ * Response text here.
66
+ * ```
67
+ */
68
+ export function parseConversationArchive(content) {
69
+ const lines = content.split(/\r?\n/);
70
+ const messages = [];
71
+ let currentDate = "";
72
+ let currentTime = "";
73
+ let currentRole = "";
74
+ let currentChannel;
75
+ let currentTextLines = [];
76
+ function flush() {
77
+ if (currentDate && currentTime && currentRole) {
78
+ const msg = {
79
+ date: currentDate,
80
+ time: currentTime,
81
+ role: currentRole,
82
+ text: currentTextLines.join("\n").trim(),
83
+ };
84
+ if (currentChannel)
85
+ msg.channel = currentChannel;
86
+ messages.push(msg);
87
+ }
88
+ currentTime = "";
89
+ currentRole = "";
90
+ currentChannel = undefined;
91
+ currentTextLines = [];
92
+ }
93
+ for (const line of lines) {
94
+ const dateMatch = line.match(DATE_HEADER);
95
+ if (dateMatch) {
96
+ flush();
97
+ currentDate = dateMatch[1];
98
+ continue;
99
+ }
100
+ const msgMatch = line.match(MESSAGE_HEADER);
101
+ if (msgMatch) {
102
+ flush();
103
+ currentTime = msgMatch[1];
104
+ currentRole = msgMatch[2];
105
+ currentChannel = msgMatch[3] ?? undefined;
106
+ continue;
107
+ }
108
+ // Accumulate text lines for the current message
109
+ if (currentRole) {
110
+ currentTextLines.push(line);
111
+ }
112
+ }
113
+ // Flush final message
114
+ flush();
115
+ return messages;
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // File resolution
119
+ // ---------------------------------------------------------------------------
120
+ /**
121
+ * Resolve the memory subdirectory for a given source identifier.
122
+ */
123
+ function resolveArchiveSubdir(params) {
124
+ const { source, sessionKey, isAdminAgent } = params;
125
+ if (source === "admin") {
126
+ return { subdir: "admin" };
127
+ }
128
+ // Phone number — DM archive
129
+ if (source.startsWith("+")) {
130
+ const subdir = isAdminAgent ? "admin" : `users/${source}`;
131
+ return { subdir };
132
+ }
133
+ // Group JID
134
+ if (source.includes("@g.us")) {
135
+ return { subdir: `groups/${source}` };
136
+ }
137
+ // "current" — resolve from session key
138
+ if (source === "current" || !source) {
139
+ if (!sessionKey) {
140
+ return { subdir: "", error: "no session key available to resolve current conversation" };
141
+ }
142
+ const parts = sessionKey.toLowerCase().split(":").filter(Boolean);
143
+ // admin webchat: agent:{agentId}:main
144
+ if (parts.length === 3 && parts[0] === "agent" && parts[2] === "main") {
145
+ return { subdir: "admin" };
146
+ }
147
+ // DM: agent:{agentId}:dm:{peer} or agent:{agentId}:{channel}:dm:{peer}
148
+ if (parts.length >= 4 && parts[2] === "dm") {
149
+ const peer = parts.slice(3).join(":");
150
+ return { subdir: isAdminAgent ? "admin" : `users/${peer}` };
151
+ }
152
+ if (parts.length >= 5 && parts[3] === "dm") {
153
+ const peer = parts.slice(4).join(":");
154
+ return { subdir: isAdminAgent ? "admin" : `users/${peer}` };
155
+ }
156
+ // Group: agent:{agentId}:{channel}:group:{groupId}
157
+ if (parts.length >= 5 && parts[3] === "group") {
158
+ const groupId = parts.slice(4).join(":");
159
+ return { subdir: `groups/${groupId}` };
160
+ }
161
+ return {
162
+ subdir: "",
163
+ error: `cannot resolve conversation source from session key: ${sessionKey}`,
164
+ };
165
+ }
166
+ return {
167
+ subdir: "",
168
+ error: `unrecognised source: "${source}". Use "current", "admin", a phone number, or a group JID.`,
169
+ };
170
+ }
171
+ /**
172
+ * List conversation archive files in a directory, sorted newest first.
173
+ */
174
+ async function listArchiveFiles(conversationsDir) {
175
+ try {
176
+ const entries = await fs.readdir(conversationsDir);
177
+ const mdFiles = entries
178
+ .filter((e) => e.endsWith(".md"))
179
+ .sort()
180
+ .reverse();
181
+ return mdFiles.map((f) => path.join(conversationsDir, f));
182
+ }
183
+ catch {
184
+ return [];
185
+ }
186
+ }
187
+ function applyFilters(messages, filters) {
188
+ let result = messages;
189
+ if (filters.dateFilter) {
190
+ const d = filters.dateFilter;
191
+ result = result.filter((m) => m.date === d);
192
+ }
193
+ if (filters.searchFilter) {
194
+ const s = filters.searchFilter.toLowerCase();
195
+ result = result.filter((m) => m.text.toLowerCase().includes(s));
196
+ }
197
+ if (filters.afterFilter) {
198
+ const a = filters.afterFilter;
199
+ result = result.filter((m) => m.time >= a);
200
+ }
201
+ if (filters.beforeFilter) {
202
+ const b = filters.beforeFilter;
203
+ result = result.filter((m) => m.time < b);
204
+ }
205
+ if (filters.channelFilter) {
206
+ const c = filters.channelFilter.toLowerCase();
207
+ result = result.filter((m) => m.channel?.toLowerCase() === c);
208
+ }
209
+ return result;
210
+ }
211
+ /**
212
+ * Scan memory directory for available conversation sources.
213
+ * Admin agents see admin/, users/*, groups/*.
214
+ * Public agents see users/*.
215
+ */
216
+ async function discoverSources(params) {
217
+ const { memoryDir, isAdminAgent } = params;
218
+ const sources = [];
219
+ // Admin conversations
220
+ if (isAdminAgent) {
221
+ const adminConvDir = path.join(memoryDir, "admin", "conversations");
222
+ const adminFiles = await listArchiveFiles(adminConvDir);
223
+ if (adminFiles.length > 0) {
224
+ sources.push({
225
+ source: "admin",
226
+ type: "admin",
227
+ label: "Admin / Webchat",
228
+ lastArchive: path.basename(adminFiles[0], ".md"),
229
+ });
230
+ }
231
+ }
232
+ // User conversations
233
+ const usersDir = path.join(memoryDir, "users");
234
+ try {
235
+ const userEntries = await fs.readdir(usersDir, { withFileTypes: true });
236
+ for (const entry of userEntries) {
237
+ if (!entry.isDirectory())
238
+ continue;
239
+ const convDir = path.join(usersDir, entry.name, "conversations");
240
+ const files = await listArchiveFiles(convDir);
241
+ if (files.length > 0) {
242
+ sources.push({
243
+ source: entry.name,
244
+ type: "user",
245
+ label: entry.name,
246
+ lastArchive: path.basename(files[0], ".md"),
247
+ });
248
+ }
249
+ }
250
+ }
251
+ catch {
252
+ // users/ doesn't exist
253
+ }
254
+ // Group conversations
255
+ const groupsDir = path.join(memoryDir, "groups");
256
+ try {
257
+ const groupEntries = await fs.readdir(groupsDir, { withFileTypes: true });
258
+ for (const entry of groupEntries) {
259
+ if (!entry.isDirectory())
260
+ continue;
261
+ const convDir = path.join(groupsDir, entry.name, "conversations");
262
+ const files = await listArchiveFiles(convDir);
263
+ if (files.length > 0) {
264
+ sources.push({
265
+ source: entry.name,
266
+ type: "group",
267
+ label: entry.name,
268
+ lastArchive: path.basename(files[0], ".md"),
269
+ });
270
+ }
271
+ }
272
+ }
273
+ catch {
274
+ // groups/ doesn't exist
275
+ }
276
+ return sources;
277
+ }
278
+ async function readAllConversations(params) {
279
+ const sources = await discoverSources({
280
+ memoryDir: params.memoryDir,
281
+ isAdminAgent: params.isAdminAgent,
282
+ });
283
+ const all = [];
284
+ for (const src of sources) {
285
+ let convDir;
286
+ if (src.type === "admin") {
287
+ convDir = path.join(params.memoryDir, "admin", "conversations");
288
+ }
289
+ else if (src.type === "user") {
290
+ convDir = path.join(params.memoryDir, "users", src.source, "conversations");
291
+ }
292
+ else {
293
+ convDir = path.join(params.memoryDir, "groups", src.source, "conversations");
294
+ }
295
+ const archiveFiles = await listArchiveFiles(convDir);
296
+ for (const filePath of archiveFiles) {
297
+ try {
298
+ const content = await fs.readFile(filePath, "utf-8");
299
+ const parsed = parseConversationArchive(content);
300
+ const filtered = applyFilters(parsed, params.filters);
301
+ for (const m of filtered) {
302
+ all.push({ ...m, source: src.source });
303
+ }
304
+ }
305
+ catch {
306
+ // skip unreadable
307
+ }
308
+ }
309
+ }
310
+ // Sort by date+time descending, take the most recent N, then reverse to chronological
311
+ all.sort((a, b) => {
312
+ const cmp = `${a.date}T${a.time}`.localeCompare(`${b.date}T${b.time}`);
313
+ return -cmp; // newest first
314
+ });
315
+ const sliced = all.slice(0, params.clampedLimit);
316
+ sliced.reverse(); // back to chronological
317
+ return sliced;
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Tool factory
321
+ // ---------------------------------------------------------------------------
322
+ export function createMessageHistoryTool(options) {
323
+ const cfg = options.config;
324
+ if (!cfg)
325
+ return null;
326
+ const agentId = resolveSessionAgentId({
327
+ sessionKey: options.agentSessionKey,
328
+ config: cfg,
329
+ });
330
+ const agentConfig = cfg?.agents?.list?.find((a) => a.id?.toLowerCase() === agentId?.toLowerCase());
331
+ const isAdminAgent = agentConfig?.tools?.profile === "full" || agentConfig?.default === true;
332
+ return {
333
+ label: "Message History",
334
+ name: "message_history",
335
+ description: "Retrieve recent messages from conversation archives with accurate timestamps. " +
336
+ "Use this to look up what was said in any conversation — current session, admin/webchat, " +
337
+ "a specific WhatsApp DM (by phone number), or a group chat (by group JID). " +
338
+ 'Pass source="discover" to list all available conversations, or source="all" to get ' +
339
+ "messages across all conversations. Supports text search, time-range, and channel filtering.",
340
+ parameters: MessageHistorySchema,
341
+ execute: async (_toolCallId, params) => {
342
+ const p = params;
343
+ const rawSource = readStringParam(p, "source") ?? "current";
344
+ const limit = readNumberParam(p, "limit") ?? 30;
345
+ const dateFilter = readStringParam(p, "date");
346
+ const searchFilter = readStringParam(p, "search");
347
+ const afterFilter = readStringParam(p, "after");
348
+ const beforeFilter = readStringParam(p, "before");
349
+ const channelFilter = readStringParam(p, "channel");
350
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
351
+ if (!workspaceDir) {
352
+ return jsonResult({ messages: [], error: "no workspace directory configured" });
353
+ }
354
+ const memoryDir = path.join(workspaceDir, "memory");
355
+ const clampedLimit = Math.min(Math.max(1, limit), 500);
356
+ const filters = {
357
+ dateFilter,
358
+ searchFilter,
359
+ afterFilter,
360
+ beforeFilter,
361
+ channelFilter,
362
+ };
363
+ // -- Discovery mode --
364
+ if (rawSource === "discover") {
365
+ const sources = await discoverSources({ memoryDir, isAdminAgent });
366
+ return jsonResult({
367
+ mode: "discover",
368
+ conversations: sources,
369
+ total: sources.length,
370
+ });
371
+ }
372
+ // -- Aggregate mode --
373
+ if (rawSource === "all") {
374
+ const messages = await readAllConversations({
375
+ memoryDir,
376
+ isAdminAgent,
377
+ clampedLimit,
378
+ filters,
379
+ });
380
+ return jsonResult({
381
+ source: "all",
382
+ messages,
383
+ total: messages.length,
384
+ ...(dateFilter ? { dateFilter } : {}),
385
+ ...(searchFilter ? { searchFilter } : {}),
386
+ ...(channelFilter ? { channelFilter } : {}),
387
+ });
388
+ }
389
+ // -- Single-source mode --
390
+ const { subdir, error: subdirError } = resolveArchiveSubdir({
391
+ source: rawSource,
392
+ sessionKey: options.agentSessionKey,
393
+ isAdminAgent,
394
+ });
395
+ if (subdirError || !subdir) {
396
+ return jsonResult({ messages: [], error: subdirError });
397
+ }
398
+ const conversationsDir = path.join(memoryDir, subdir, "conversations");
399
+ const archiveFiles = await listArchiveFiles(conversationsDir);
400
+ if (archiveFiles.length === 0) {
401
+ return jsonResult({
402
+ messages: [],
403
+ source: rawSource,
404
+ info: `no conversation archives found in ${subdir}/conversations/`,
405
+ });
406
+ }
407
+ // Collect messages from archive files (newest files first)
408
+ const allMessages = [];
409
+ for (const filePath of archiveFiles) {
410
+ try {
411
+ const content = await fs.readFile(filePath, "utf-8");
412
+ const parsed = parseConversationArchive(content);
413
+ const filtered = applyFilters(parsed, filters);
414
+ allMessages.unshift(...filtered);
415
+ // If we have enough messages and no active filters require scanning all files, stop early
416
+ if (!dateFilter && !searchFilter && allMessages.length >= clampedLimit) {
417
+ break;
418
+ }
419
+ }
420
+ catch {
421
+ // Skip unreadable files
422
+ }
423
+ }
424
+ // Return the most recent N messages
425
+ const result = allMessages.slice(-clampedLimit);
426
+ return jsonResult({
427
+ source: rawSource,
428
+ messages: result,
429
+ total: result.length,
430
+ ...(dateFilter ? { dateFilter } : {}),
431
+ ...(searchFilter ? { searchFilter } : {}),
432
+ ...(channelFilter ? { channelFilter } : {}),
433
+ });
434
+ },
435
+ };
436
+ }
@@ -111,6 +111,7 @@ export function createSessionsHistoryTool(opts) {
111
111
  params: {
112
112
  sessionKey: resolvedKey,
113
113
  limit,
114
+ preserveEnvelopes: true,
114
115
  ...(sessionIdParam ? { sessionId: sessionIdParam } : {}),
115
116
  },
116
117
  }));
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.63",
3
- "commit": "15a1377031320e9d047b3aba1bcc9124e5b707d0",
4
- "builtAt": "2026-02-18T00:18:16.896Z"
2
+ "version": "1.0.65",
3
+ "commit": "d9051199b654984c06db2b45b2b5712191e13a2c",
4
+ "builtAt": "2026-02-18T16:08:39.070Z"
5
5
  }
@@ -553,6 +553,16 @@ export const TaskmasterSchema = z
553
553
  })
554
554
  .strict()
555
555
  .optional(),
556
+ publicChat: z
557
+ .object({
558
+ enabled: z.boolean().optional(),
559
+ auth: z
560
+ .union([z.literal("anonymous"), z.literal("verified"), z.literal("choice")])
561
+ .optional(),
562
+ cookieTtlDays: z.number().int().positive().optional(),
563
+ })
564
+ .strict()
565
+ .optional(),
556
566
  })
557
567
  .strict()
558
568
  .superRefine((cfg, ctx) => {