@rubytech/taskmaster 1.3.0 → 1.4.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/auth-profiles/oauth.js +24 -0
- package/dist/agents/tools/message-history-tool.js +2 -3
- package/dist/auto-reply/media-note.js +11 -0
- package/dist/auto-reply/reply/get-reply.js +4 -0
- package/dist/build-info.json +3 -3
- package/dist/control-ui/assets/{index-DQ1kxYd4.js → index-BDETQp97.js} +281 -283
- package/dist/control-ui/assets/index-BDETQp97.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/chat-sanitize.js +5 -1
- package/dist/gateway/server/tls.js +2 -2
- package/dist/gateway/server-http.js +34 -4
- package/dist/gateway/server-methods/chat.js +64 -25
- package/dist/gateway/server.impl.js +23 -5
- package/dist/infra/tls/gateway.js +19 -3
- package/dist/memory/audit.js +9 -0
- package/dist/memory/manager.js +1 -1
- package/dist/web/auto-reply/monitor/process-message.js +44 -17
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -0
- package/extensions/googlechat/node_modules/.bin/taskmaster +0 -0
- package/extensions/line/node_modules/.bin/taskmaster +0 -0
- package/extensions/matrix/node_modules/.bin/markdown-it +0 -0
- package/extensions/matrix/node_modules/.bin/taskmaster +0 -0
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -0
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -0
- package/extensions/msteams/node_modules/.bin/taskmaster +0 -0
- package/extensions/nostr/node_modules/.bin/taskmaster +0 -0
- package/extensions/nostr/node_modules/.bin/tsc +0 -0
- package/extensions/nostr/node_modules/.bin/tsserver +0 -0
- package/extensions/zalo/node_modules/.bin/taskmaster +0 -0
- package/extensions/zalouser/node_modules/.bin/taskmaster +0 -0
- package/package.json +54 -64
- package/scripts/install.sh +0 -0
- package/taskmaster-docs/USER-GUIDE.md +28 -2
- package/templates/.DS_Store +0 -0
- package/templates/customer/.DS_Store +0 -0
- package/templates/customer/agents/.DS_Store +0 -0
- package/templates/maxy/.DS_Store +0 -0
- package/templates/maxy/.gitignore +1 -0
- package/templates/maxy/agents/.DS_Store +0 -0
- package/templates/maxy/agents/admin/.DS_Store +0 -0
- package/templates/maxy/memory/.DS_Store +0 -0
- package/templates/maxy/skills/.DS_Store +0 -0
- package/templates/taskmaster/.gitignore +1 -0
- package/dist/control-ui/assets/index-DQ1kxYd4.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BDETQp97.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-CPawOl_z.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -208,6 +208,8 @@ export function mimeFromExt(ext) {
|
|
|
208
208
|
return "audio/wav";
|
|
209
209
|
case "m4a":
|
|
210
210
|
return "audio/mp4";
|
|
211
|
+
case "webm":
|
|
212
|
+
return "audio/webm";
|
|
211
213
|
default:
|
|
212
214
|
return "application/octet-stream";
|
|
213
215
|
}
|
|
@@ -243,7 +245,9 @@ function mediaRefToUrl(ref, workspaceRoot) {
|
|
|
243
245
|
const stat = nodeFs.statSync(ref.absPath);
|
|
244
246
|
mtime = `&t=${stat.mtimeMs | 0}`;
|
|
245
247
|
}
|
|
246
|
-
catch {
|
|
248
|
+
catch {
|
|
249
|
+
/* file may not exist yet */
|
|
250
|
+
}
|
|
247
251
|
return `/api/media?path=${encodeURIComponent(relPath)}${mtime}`;
|
|
248
252
|
}
|
|
249
253
|
function stripBase64FromContentBlocks(content) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { loadGatewayTlsRuntime as loadGatewayTlsRuntimeConfig, } from "../../infra/tls/gateway.js";
|
|
2
|
-
export async function loadGatewayTlsRuntime(cfg, log) {
|
|
3
|
-
return await loadGatewayTlsRuntimeConfig(cfg, log);
|
|
2
|
+
export async function loadGatewayTlsRuntime(cfg, log, hostnames) {
|
|
3
|
+
return await loadGatewayTlsRuntimeConfig(cfg, log, hostnames);
|
|
4
4
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer as createHttpServer, } from "node:http";
|
|
2
2
|
import { createServer as createHttpsServer } from "node:https";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
|
|
4
5
|
import { loadConfig } from "../config/config.js";
|
|
5
6
|
import { handleSlackHttpRequest } from "../slack/http/index.js";
|
|
@@ -141,13 +142,42 @@ export function createHooksRequestHandler(opts) {
|
|
|
141
142
|
}
|
|
142
143
|
export function createGatewayHttpServer(opts) {
|
|
143
144
|
const { canvasHost, controlUiEnabled, controlUiBasePath, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, } = opts;
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
// When TLS is enabled, create a dual-protocol server: a net.Server listens
|
|
146
|
+
// on the port, peeks at the first byte of each connection to detect TLS vs
|
|
147
|
+
// plain HTTP, and dispatches accordingly. Plain HTTP gets a 301 redirect to
|
|
148
|
+
// HTTPS so bookmarks and muscle-memory URLs keep working.
|
|
149
|
+
let httpServer;
|
|
150
|
+
if (opts.tlsOptions) {
|
|
151
|
+
const httpsServer = createHttpsServer(opts.tlsOptions, (req, res) => {
|
|
146
152
|
void handleRequest(req, res);
|
|
147
|
-
})
|
|
148
|
-
|
|
153
|
+
});
|
|
154
|
+
const redirectServer = createHttpServer((req, res) => {
|
|
155
|
+
const host = req.headers.host ?? "localhost";
|
|
156
|
+
res.writeHead(301, { Location: `https://${host}${req.url ?? "/"}`, Connection: "close" });
|
|
157
|
+
res.end();
|
|
158
|
+
});
|
|
159
|
+
const tcpServer = net.createServer((socket) => {
|
|
160
|
+
socket.once("data", (buf) => {
|
|
161
|
+
socket.pause();
|
|
162
|
+
socket.unshift(buf);
|
|
163
|
+
// TLS ClientHello starts with byte 0x16 (22)
|
|
164
|
+
const target = buf[0] === 0x16 ? httpsServer : redirectServer;
|
|
165
|
+
target.emit("connection", socket);
|
|
166
|
+
process.nextTick(() => socket.resume());
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// Forward upgrade events from the HTTPS server to the TCP wrapper
|
|
170
|
+
// so WebSocket handlers attached via attachGatewayUpgradeHandler work.
|
|
171
|
+
httpsServer.on("upgrade", (req, socket, head) => {
|
|
172
|
+
tcpServer.emit("upgrade", req, socket, head);
|
|
173
|
+
});
|
|
174
|
+
httpServer = tcpServer;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
httpServer = createHttpServer((req, res) => {
|
|
149
178
|
void handleRequest(req, res);
|
|
150
179
|
});
|
|
180
|
+
}
|
|
151
181
|
async function handleRequest(req, res) {
|
|
152
182
|
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
|
|
153
183
|
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket")
|
|
@@ -379,8 +379,12 @@ export const chatHandlers = {
|
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
-
// Save document attachments to workspace uploads dir (persistent, accessible by agent)
|
|
382
|
+
// Save document attachments to workspace uploads dir (persistent, accessible by agent).
|
|
383
|
+
// Audio files are separated so they can be routed through the media understanding
|
|
384
|
+
// pipeline (STT) instead of being treated as generic file attachments.
|
|
383
385
|
const savedDocPaths = [];
|
|
386
|
+
const savedAudioPaths = [];
|
|
387
|
+
const savedAudioTypes = [];
|
|
384
388
|
if (documentAttachments.length > 0 && uploadsDir) {
|
|
385
389
|
for (const doc of documentAttachments) {
|
|
386
390
|
if (!doc.content || typeof doc.content !== "string")
|
|
@@ -389,7 +393,14 @@ export const chatHandlers = {
|
|
|
389
393
|
const destPath = path.join(uploadsDir, safeName);
|
|
390
394
|
try {
|
|
391
395
|
fs.writeFileSync(destPath, Buffer.from(doc.content, "base64"));
|
|
392
|
-
|
|
396
|
+
const mimeBase = doc.mimeType?.split(";")[0]?.trim() ?? "";
|
|
397
|
+
if (mimeBase.startsWith("audio/")) {
|
|
398
|
+
savedAudioPaths.push(destPath);
|
|
399
|
+
savedAudioTypes.push(doc.mimeType ?? "audio/webm");
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
savedDocPaths.push(destPath);
|
|
403
|
+
}
|
|
393
404
|
}
|
|
394
405
|
catch (err) {
|
|
395
406
|
context.logGateway.warn(`chat document save failed: ${String(err)}`);
|
|
@@ -460,18 +471,29 @@ export const chatHandlers = {
|
|
|
460
471
|
const trimmedMessage = p.message.trim();
|
|
461
472
|
const injectThinking = Boolean(p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"));
|
|
462
473
|
const commandBody = injectThinking ? `/think ${p.thinking} ${p.message}` : p.message;
|
|
463
|
-
// If documents were saved, prepend file paths to message
|
|
474
|
+
// If non-audio documents were saved, prepend file paths to message.
|
|
475
|
+
// Audio files are NOT annotated here — they go through MediaPaths so the
|
|
476
|
+
// media understanding pipeline (STT) handles them, and buildInboundMediaNote
|
|
477
|
+
// generates the proper [media attached: ...] annotation.
|
|
464
478
|
const docNote = savedDocPaths.length > 0
|
|
465
479
|
? savedDocPaths.map((p) => `[file: ${p}]`).join("\n") + "\n\n"
|
|
466
480
|
: "";
|
|
467
|
-
|
|
481
|
+
// Audio-only message (voice note, no text): use placeholder so
|
|
482
|
+
// applyMediaUnderstanding knows to replace with transcript or error.
|
|
483
|
+
const hasAudioMedia = savedAudioPaths.length > 0;
|
|
484
|
+
const effectiveBody = hasAudioMedia && !trimmedMessage ? "<media:audio>" : p.message;
|
|
485
|
+
const messageWithDocs = docNote + effectiveBody;
|
|
486
|
+
const effectiveCommandBody = hasAudioMedia && !trimmedMessage ? "<media:audio>" : commandBody;
|
|
487
|
+
// Merge image and audio paths so the media understanding pipeline sees both.
|
|
488
|
+
const allMediaPaths = [...savedImagePaths, ...savedAudioPaths];
|
|
489
|
+
const allMediaTypes = [...savedImageTypes, ...savedAudioTypes];
|
|
468
490
|
const clientInfo = client?.connect?.client;
|
|
469
491
|
const ctx = {
|
|
470
492
|
Body: messageWithDocs,
|
|
471
493
|
BodyForAgent: messageWithDocs,
|
|
472
|
-
BodyForCommands: docNote +
|
|
494
|
+
BodyForCommands: docNote + effectiveCommandBody,
|
|
473
495
|
RawBody: messageWithDocs,
|
|
474
|
-
CommandBody: docNote +
|
|
496
|
+
CommandBody: docNote + effectiveCommandBody,
|
|
475
497
|
SessionKey: p.sessionKey,
|
|
476
498
|
Provider: INTERNAL_MESSAGE_CHANNEL,
|
|
477
499
|
Surface: INTERNAL_MESSAGE_CHANNEL,
|
|
@@ -485,10 +507,10 @@ export const chatHandlers = {
|
|
|
485
507
|
// Image/media paths — same pattern as WhatsApp. buildInboundMediaNote()
|
|
486
508
|
// will generate [media attached: ...] annotations that the agent runner
|
|
487
509
|
// detects and loads from disk at inference time.
|
|
488
|
-
MediaPaths:
|
|
489
|
-
MediaPath:
|
|
490
|
-
MediaTypes:
|
|
491
|
-
MediaType:
|
|
510
|
+
MediaPaths: allMediaPaths.length > 0 ? allMediaPaths : undefined,
|
|
511
|
+
MediaPath: allMediaPaths[0],
|
|
512
|
+
MediaTypes: allMediaTypes.length > 0 ? allMediaTypes : undefined,
|
|
513
|
+
MediaType: allMediaTypes[0],
|
|
492
514
|
};
|
|
493
515
|
const agentId = resolveSessionAgentId({
|
|
494
516
|
sessionKey: p.sessionKey,
|
|
@@ -496,16 +518,26 @@ export const chatHandlers = {
|
|
|
496
518
|
});
|
|
497
519
|
// Fire message:inbound hook for conversation archiving.
|
|
498
520
|
// Include image paths so the archive references the attached media.
|
|
521
|
+
// Audio archive is deferred until after media understanding resolves (see
|
|
522
|
+
// onMediaResolved below) so the transcript is available instead of the
|
|
523
|
+
// raw <media:audio> placeholder.
|
|
499
524
|
const imageNote = savedImagePaths.length > 0 ? savedImagePaths.map((ip) => `[image: ${ip}]`).join("\n") : "";
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
525
|
+
const fireArchiveHook = (resolvedBody) => {
|
|
526
|
+
const body = resolvedBody ?? p.message;
|
|
527
|
+
const archiveText = [body, imageNote].filter(Boolean).join("\n").trim();
|
|
528
|
+
void triggerInternalHook(createInternalHookEvent("message", "inbound", p.sessionKey, {
|
|
529
|
+
text: archiveText || undefined,
|
|
530
|
+
timestamp: now,
|
|
531
|
+
chatType: "direct",
|
|
532
|
+
agentId,
|
|
533
|
+
channel: "webchat",
|
|
534
|
+
cfg,
|
|
535
|
+
}));
|
|
536
|
+
};
|
|
537
|
+
if (!hasAudioMedia) {
|
|
538
|
+
// No audio — fire immediately (no STT to wait for).
|
|
539
|
+
fireArchiveHook();
|
|
540
|
+
}
|
|
509
541
|
let prefixContext = {
|
|
510
542
|
identityName: resolveIdentityName(cfg, agentId),
|
|
511
543
|
};
|
|
@@ -541,7 +573,7 @@ export const chatHandlers = {
|
|
|
541
573
|
},
|
|
542
574
|
});
|
|
543
575
|
let agentRunStarted = false;
|
|
544
|
-
context.logGateway.info(`webchat dispatch: sessionKey=${p.sessionKey} runId=${clientRunId} body=${messageWithDocs.length}ch images=${savedImagePaths.length} docs=${savedDocPaths.length}`);
|
|
576
|
+
context.logGateway.info(`webchat dispatch: sessionKey=${p.sessionKey} runId=${clientRunId} body=${messageWithDocs.length}ch images=${savedImagePaths.length} audio=${savedAudioPaths.length} docs=${savedDocPaths.length}`);
|
|
545
577
|
void dispatchInboundMessage({
|
|
546
578
|
ctx,
|
|
547
579
|
cfg,
|
|
@@ -554,11 +586,18 @@ export const chatHandlers = {
|
|
|
554
586
|
agentRunStarted = true;
|
|
555
587
|
context.logGateway.info(`webchat agent run started: sessionKey=${p.sessionKey} runId=${runId}`);
|
|
556
588
|
},
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
589
|
+
onMediaResolved: hasAudioMedia
|
|
590
|
+
? () => {
|
|
591
|
+
// STT complete — archive the resolved body (transcript) instead
|
|
592
|
+
// of the raw <media:audio> placeholder.
|
|
593
|
+
fireArchiveHook(ctx.Body);
|
|
594
|
+
}
|
|
595
|
+
: undefined,
|
|
596
|
+
onModelSelected: (modelCtx) => {
|
|
597
|
+
prefixContext.provider = modelCtx.provider;
|
|
598
|
+
prefixContext.model = extractShortModelName(modelCtx.model);
|
|
599
|
+
prefixContext.modelFull = `${modelCtx.provider}/${modelCtx.model}`;
|
|
600
|
+
prefixContext.thinkingLevel = modelCtx.thinkLevel ?? "off";
|
|
562
601
|
},
|
|
563
602
|
},
|
|
564
603
|
})
|
|
@@ -54,6 +54,7 @@ import { ensureWatchdogUnitOnStartup, scheduleWatchdogStabilityConfirmation, } f
|
|
|
54
54
|
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
|
|
55
55
|
import { startWifiWatchdog } from "./server-wifi-watchdog.js";
|
|
56
56
|
import { loadGatewayTlsRuntime } from "./server/tls.js";
|
|
57
|
+
import { isLoopbackHost } from "./net.js";
|
|
57
58
|
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
|
|
58
59
|
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
|
|
59
60
|
import { isLicenseValid } from "../license/validate.js";
|
|
@@ -226,10 +227,30 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
226
227
|
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
|
|
227
228
|
const deps = createDefaultDeps();
|
|
228
229
|
let canvasHostServer = null;
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
// Auto-enable TLS when binding to a non-loopback address (LAN, custom, etc.)
|
|
231
|
+
// so that browser secure-context APIs (getUserMedia, etc.) work over .local.
|
|
232
|
+
// Only auto-enable when the user hasn't explicitly configured tls.enabled.
|
|
233
|
+
const tlsExplicit = cfgAtStart.gateway?.tls?.enabled;
|
|
234
|
+
const tlsAutoEnable = tlsExplicit === undefined && !isLoopbackHost(bindHost);
|
|
235
|
+
const effectiveTlsConfig = tlsAutoEnable
|
|
236
|
+
? { ...cfgAtStart.gateway?.tls, enabled: true }
|
|
237
|
+
: cfgAtStart.gateway?.tls;
|
|
238
|
+
if (tlsAutoEnable) {
|
|
239
|
+
log.child("tls").info("gateway tls: auto-enabled for non-loopback bind");
|
|
240
|
+
}
|
|
241
|
+
const bonjourHostname = cfgAtStart.discovery?.bonjourHostname || "taskmaster";
|
|
242
|
+
const tlsHostnames = [bonjourHostname];
|
|
243
|
+
const gatewayTls = await loadGatewayTlsRuntime(effectiveTlsConfig, log.child("tls"), tlsHostnames);
|
|
244
|
+
if (tlsExplicit === true && !gatewayTls.enabled) {
|
|
245
|
+
// User explicitly enabled TLS — fail hard if it can't start.
|
|
231
246
|
throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
|
|
232
247
|
}
|
|
248
|
+
if (tlsAutoEnable && !gatewayTls.enabled) {
|
|
249
|
+
// Auto-enabled TLS failed — fall back to HTTP with a warning.
|
|
250
|
+
log
|
|
251
|
+
.child("tls")
|
|
252
|
+
.warn(`gateway tls: auto-enable failed (${gatewayTls.error ?? "unknown"}), continuing with HTTP`);
|
|
253
|
+
}
|
|
233
254
|
const { canvasHost, httpServer, httpServers, httpBindHosts, wss, clients, broadcast, agentRunSeq, dedupe, chatRunState, chatRunBuffers, chatDeltaSentAt, addChatRun, removeChatRun, chatAbortControllers, } = await createGatewayRuntimeState({
|
|
234
255
|
cfg: cfgAtStart,
|
|
235
256
|
bindHost,
|
|
@@ -283,9 +304,6 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
283
304
|
});
|
|
284
305
|
const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;
|
|
285
306
|
const machineDisplayName = await getMachineDisplayName();
|
|
286
|
-
// Default to "taskmaster" hostname for mDNS so taskmaster.local works out of the box.
|
|
287
|
-
// Users can override via discovery.bonjourHostname config if needed.
|
|
288
|
-
const bonjourHostname = cfgAtStart.discovery?.bonjourHostname || "taskmaster";
|
|
289
307
|
const discovery = await startGatewayDiscovery({
|
|
290
308
|
machineDisplayName,
|
|
291
309
|
port,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { X509Certificate } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
import { CONFIG_DIR, ensureDir, resolveUserPath, shortenHomeInString } from "../../utils.js";
|
|
@@ -15,6 +16,18 @@ async function fileExists(filePath) {
|
|
|
15
16
|
return false;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
function buildSanString(hostnames) {
|
|
20
|
+
const sans = new Set(["DNS:localhost", "IP:127.0.0.1"]);
|
|
21
|
+
const raw = hostnames?.length ? hostnames : [os.hostname()];
|
|
22
|
+
for (const h of raw) {
|
|
23
|
+
const name = h.replace(/\.local$/i, "").trim();
|
|
24
|
+
if (!name)
|
|
25
|
+
continue;
|
|
26
|
+
sans.add(`DNS:${name}`);
|
|
27
|
+
sans.add(`DNS:${name}.local`);
|
|
28
|
+
}
|
|
29
|
+
return [...sans].join(",");
|
|
30
|
+
}
|
|
18
31
|
async function generateSelfSignedCert(params) {
|
|
19
32
|
const certDir = path.dirname(params.certPath);
|
|
20
33
|
const keyDir = path.dirname(params.keyPath);
|
|
@@ -22,6 +35,7 @@ async function generateSelfSignedCert(params) {
|
|
|
22
35
|
if (keyDir !== certDir) {
|
|
23
36
|
await ensureDir(keyDir);
|
|
24
37
|
}
|
|
38
|
+
const san = buildSanString(params.hostnames);
|
|
25
39
|
await execFileAsync("openssl", [
|
|
26
40
|
"req",
|
|
27
41
|
"-x509",
|
|
@@ -37,12 +51,14 @@ async function generateSelfSignedCert(params) {
|
|
|
37
51
|
params.certPath,
|
|
38
52
|
"-subj",
|
|
39
53
|
"/CN=taskmaster-gateway",
|
|
54
|
+
"-addext",
|
|
55
|
+
`subjectAltName=${san}`,
|
|
40
56
|
]);
|
|
41
57
|
await fs.chmod(params.keyPath, 0o600).catch(() => { });
|
|
42
58
|
await fs.chmod(params.certPath, 0o600).catch(() => { });
|
|
43
|
-
params.log?.info?.(`gateway tls: generated self-signed cert at ${shortenHomeInString(params.certPath)}`);
|
|
59
|
+
params.log?.info?.(`gateway tls: generated self-signed cert at ${shortenHomeInString(params.certPath)} (SAN: ${san})`);
|
|
44
60
|
}
|
|
45
|
-
export async function loadGatewayTlsRuntime(cfg, log) {
|
|
61
|
+
export async function loadGatewayTlsRuntime(cfg, log, hostnames) {
|
|
46
62
|
if (!cfg || cfg.enabled !== true)
|
|
47
63
|
return { enabled: false, required: false };
|
|
48
64
|
const autoGenerate = cfg.autoGenerate !== false;
|
|
@@ -54,7 +70,7 @@ export async function loadGatewayTlsRuntime(cfg, log) {
|
|
|
54
70
|
const hasKey = await fileExists(keyPath);
|
|
55
71
|
if (!hasCert && !hasKey && autoGenerate) {
|
|
56
72
|
try {
|
|
57
|
-
await generateSelfSignedCert({ certPath, keyPath, log });
|
|
73
|
+
await generateSelfSignedCert({ certPath, keyPath, hostnames, log });
|
|
58
74
|
}
|
|
59
75
|
catch (err) {
|
|
60
76
|
return {
|
package/dist/memory/audit.js
CHANGED
|
@@ -55,11 +55,20 @@ function writeAuditFile(workspaceDir, data) {
|
|
|
55
55
|
// Audit is best-effort — don't fail writes over audit persistence
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** Deduplicate window: ignore a second write to the same path by the same agent within this period. */
|
|
59
|
+
const DEDUP_WINDOW_MS = 60_000;
|
|
58
60
|
/**
|
|
59
61
|
* Record an audit entry for a memory write.
|
|
62
|
+
* Skips the entry if an identical path+agent combination was recorded within the dedup window.
|
|
60
63
|
*/
|
|
61
64
|
export function recordAuditEntry(workspaceDir, entry) {
|
|
62
65
|
const audit = readAuditFile(workspaceDir);
|
|
66
|
+
// Deduplicate: skip if same path + agent recorded within the last DEDUP_WINDOW_MS
|
|
67
|
+
const dominated = audit.entries.some((e) => e.path === entry.path &&
|
|
68
|
+
e.agentId === entry.agentId &&
|
|
69
|
+
entry.timestamp - e.timestamp < DEDUP_WINDOW_MS);
|
|
70
|
+
if (dominated)
|
|
71
|
+
return;
|
|
63
72
|
audit.entries.push(entry);
|
|
64
73
|
// Cap at 500 entries to prevent unbounded growth.
|
|
65
74
|
if (audit.entries.length > 500) {
|
package/dist/memory/manager.js
CHANGED
|
@@ -1245,7 +1245,7 @@ export class MemoryIndexManager {
|
|
|
1245
1245
|
}
|
|
1246
1246
|
const action = record ? "updated" : "added";
|
|
1247
1247
|
log.info(`file ${action} (${this.agentId}): ${entry.path}`);
|
|
1248
|
-
if (isAuditablePath(entry.path)) {
|
|
1248
|
+
if (isAuditablePath(entry.path) && record?.hash !== entry.hash) {
|
|
1249
1249
|
recordAuditEntry(this.workspaceDir, {
|
|
1250
1250
|
path: entry.path,
|
|
1251
1251
|
timestamp: Date.now(),
|
|
@@ -130,23 +130,32 @@ export async function processMessage(params) {
|
|
|
130
130
|
params.echoForget(combinedEchoKey);
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
133
|
-
// Fire message:inbound hook for conversation archiving
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
// Fire message:inbound hook for conversation archiving.
|
|
134
|
+
// For media messages (audio, image, video) the raw body is a placeholder like
|
|
135
|
+
// `<media:audio>`. Defer the hook until after media understanding resolves so
|
|
136
|
+
// the archive records the transcript / description instead of the dead link.
|
|
137
|
+
// Text-only messages fire immediately (unchanged behaviour).
|
|
138
|
+
const hasMediaAttachment = Boolean(params.msg.mediaType);
|
|
139
|
+
const fireInboundArchiveHook = (text) => {
|
|
140
|
+
void triggerInternalHook(createInternalHookEvent("message", "inbound", params.route.sessionKey, {
|
|
141
|
+
from: params.msg.senderE164 ?? params.msg.from,
|
|
142
|
+
to: params.msg.to,
|
|
143
|
+
text,
|
|
144
|
+
timestamp: params.msg.timestamp ?? Date.now(),
|
|
145
|
+
chatType: params.msg.chatType,
|
|
146
|
+
agentId: params.route.agentId,
|
|
147
|
+
channel: "whatsapp",
|
|
148
|
+
mediaType: params.msg.mediaType,
|
|
149
|
+
senderName: params.msg.senderName,
|
|
150
|
+
senderE164: params.msg.senderE164,
|
|
151
|
+
groupSubject: params.msg.groupSubject,
|
|
152
|
+
conversationId,
|
|
153
|
+
cfg: params.cfg,
|
|
154
|
+
}));
|
|
155
|
+
};
|
|
156
|
+
if (!hasMediaAttachment) {
|
|
157
|
+
fireInboundArchiveHook(params.msg.body);
|
|
158
|
+
}
|
|
150
159
|
// Send ack reaction immediately upon message receipt (post-gating).
|
|
151
160
|
// Suppress when running silently on un-mentioned group messages.
|
|
152
161
|
if (params.suppressDelivery) {
|
|
@@ -364,6 +373,24 @@ export async function processMessage(params) {
|
|
|
364
373
|
? !params.cfg.channels.whatsapp.blockStreaming
|
|
365
374
|
: undefined,
|
|
366
375
|
onModelSelected: prefixContext.onModelSelected,
|
|
376
|
+
// For media messages the inbound archive hook was deferred until after
|
|
377
|
+
// media understanding resolves. Fire it now with the resolved text.
|
|
378
|
+
...(hasMediaAttachment
|
|
379
|
+
? {
|
|
380
|
+
onMediaResolved: () => {
|
|
381
|
+
const raw = params.msg.body ?? "";
|
|
382
|
+
// Prefer the raw transcript for audio. For other media or failed
|
|
383
|
+
// transcription fall back to the resolved Body (which contains the
|
|
384
|
+
// description or a human-readable failure reason). The regex guard
|
|
385
|
+
// prevents using the envelope-formatted Body for plain text messages.
|
|
386
|
+
const resolvedText = ctxPayload.Transcript ??
|
|
387
|
+
(/^<media:[^>]+>/i.test(raw.trim())
|
|
388
|
+
? String(ctxPayload.Body ?? raw)
|
|
389
|
+
: raw);
|
|
390
|
+
fireInboundArchiveHook(String(resolvedText));
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
: {}),
|
|
367
394
|
},
|
|
368
395
|
});
|
|
369
396
|
if (!queuedFinal) {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|