@rubytech/taskmaster 1.12.3 → 1.13.1
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/consolidate.js +72 -0
- package/dist/agents/auth-profiles/oauth.js +0 -24
- package/dist/agents/auth-profiles/paths.js +4 -4
- package/dist/agents/auth-profiles/store.js +8 -100
- package/dist/agents/model-fallback.js +26 -1
- package/dist/agents/pi-embedded-runner/run/payloads.js +8 -0
- package/dist/agents/session-transcript-repair.js +3 -2
- package/dist/agents/system-prompt.js +1 -0
- package/dist/agents/taskmaster-tools.js +2 -0
- package/dist/agents/tool-policy.js +2 -0
- package/dist/agents/tools/opening-hours-tool.js +92 -0
- package/dist/agents/tools/web-fetch.js +8 -3
- package/dist/agents/tools/web-search.js +7 -4
- package/dist/agents/workspace-migrations.js +47 -0
- package/dist/build-info.json +3 -3
- package/dist/commands/agents.commands.add.js +1 -32
- package/dist/config/defaults.js +1 -1
- package/dist/config/legacy.migrations.part-3.js +25 -4
- package/dist/config/sessions/transcript.js +31 -0
- package/dist/config/types.business.js +1 -0
- package/dist/config/zod-schema.js +33 -0
- package/dist/control-ui/assets/{index-CpaEIgQy.css → index-B8I8lMfz.css} +1 -1
- package/dist/control-ui/assets/{index-CP9IoaZp.js → index-BWqMMgRV.js} +537 -425
- package/dist/control-ui/assets/index-BWqMMgRV.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/server-close.js +8 -0
- package/dist/gateway/server-methods/business.js +31 -0
- package/dist/gateway/server-methods/network.js +19 -6
- package/dist/gateway/server-methods/update.js +20 -3
- package/dist/gateway/server-methods.js +5 -1
- package/dist/gateway/server.impl.js +42 -0
- package/dist/infra/heartbeat-infra-alert.js +54 -0
- package/dist/infra/update-runner.js +27 -2
- package/dist/memory/manager.js +5 -5
- package/dist/web/auto-reply/monitor/process-message.js +24 -0
- package/dist/web/inbound/access-control.js +2 -1
- package/dist/web/inbound/monitor.js +32 -10
- package/dist/web/inbound/owner-mirror.js +35 -0
- package/package.json +1 -1
- package/skills/anthropic/SKILL.md +30 -0
- package/skills/anthropic/references/setup-guide.md +146 -0
- package/skills/google-ai/SKILL.md +3 -2
- package/skills/google-ai/references/setup-guide.md +94 -0
- package/skills/log-review/SKILL.md +45 -0
- package/skills/log-review/cron-template.json +21 -0
- package/skills/log-review/references/review-protocol.md +65 -0
- package/skills/openai/SKILL.md +28 -0
- package/skills/openai/references/setup-guide.md +122 -0
- package/taskmaster-docs/USER-GUIDE.md +31 -2
- package/templates/beagle-taxi/memory/public/investors-knowledge-base.md +230 -0
- package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +3 -1
- package/templates/customer/agents/admin/BOOTSTRAP.md +14 -2
- package/templates/customer/agents/public/AGENTS.md +15 -0
- package/templates/education-hero/agents/admin/BOOTSTRAP.md +14 -2
- package/templates/real-agent/agents/admin/AGENTS.md +139 -0
- package/templates/real-agent/agents/admin/HEARTBEAT.md +12 -0
- package/templates/real-agent/agents/admin/IDENTITY.md +11 -0
- package/templates/real-agent/agents/admin/SOUL.md +38 -0
- package/templates/real-agent/agents/public/AGENTS.md +183 -0
- package/templates/real-agent/agents/public/IDENTITY.md +8 -0
- package/templates/real-agent/agents/public/SOUL.md +75 -0
- package/templates/real-agent/memory/admin/.gitkeep +0 -0
- package/templates/real-agent/memory/public/contributors/adam-mackay.md +7 -0
- package/templates/real-agent/memory/public/contributors/alex-pelosi-buchanan.md +7 -0
- package/templates/real-agent/memory/public/contributors/jamie-fisher.md +7 -0
- package/templates/real-agent/memory/public/contributors/john-savage.md +7 -0
- package/templates/real-agent/memory/public/contributors/melanie-attwater.md +7 -0
- package/templates/real-agent/memory/public/contributors/regina-mangan.md +7 -0
- package/templates/real-agent/memory/public/contributors/richard-rawlings.md +7 -0
- package/templates/real-agent/memory/public/contributors/roger-black.md +7 -0
- package/templates/real-agent/memory/public/contributors/steve-backley.md +7 -0
- package/templates/real-agent/memory/public/courses/agency-blueprint/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/podcast/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-business/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-coaching/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-marketing/.gitkeep +0 -0
- package/templates/real-agent/memory/public/resources/.gitkeep +0 -0
- package/templates/real-agent/memory/shared/.gitkeep +0 -0
- package/templates/real-agent/memory/users/.gitkeep +0 -0
- package/templates/real-agent/skills/bespoke-coaching/SKILL.md +29 -0
- package/templates/real-agent/skills/bespoke-coaching/references/coaching-boundaries.md +56 -0
- package/templates/real-agent/skills/bespoke-coaching/references/feedback-framework.md +61 -0
- package/templates/real-agent/skills/bootstrap/SKILL.md +27 -0
- package/templates/real-agent/skills/bootstrap/references/onboarding-flow.md +63 -0
- package/templates/real-agent/skills/content-directory/SKILL.md +40 -0
- package/templates/real-agent/skills/content-directory/references/module-delivery.md +65 -0
- package/templates/real-agent/skills/content-directory/references/progress-tracking.md +47 -0
- package/templates/tradesupport/agents/admin/BOOTSTRAP.md +14 -2
- package/templates/zanzi-taxi/agents/admin/AGENTS.md +58 -0
- package/templates/zanzi-taxi/agents/admin/HEARTBEAT.md +12 -0
- package/templates/zanzi-taxi/agents/admin/IDENTITY.md +9 -0
- package/templates/zanzi-taxi/agents/admin/SOUL.md +33 -0
- package/templates/zanzi-taxi/agents/public/AGENTS.md +71 -0
- package/templates/zanzi-taxi/agents/public/IDENTITY.md +8 -0
- package/templates/zanzi-taxi/agents/public/SOUL.md +58 -0
- package/templates/zanzi-taxi/memory/public/knowledge-base.md +156 -0
- package/templates/zanzi-taxi/skills/zanzi-taxi/SKILL.md +39 -0
- package/templates/zanzi-taxi/skills/zanzi-taxi/references/local-knowledge.md +32 -0
- package/templates/zanzi-taxi/skills/zanzi-taxi/references/post-ride.md +42 -0
- package/templates/zanzi-taxi/skills/zanzi-taxi/references/ride-matching.md +74 -0
- package/dist/control-ui/assets/index-CP9IoaZp.js.map +0 -1
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
- package/extensions/googlechat/node_modules/.bin/taskmaster +0 -21
- package/extensions/line/node_modules/.bin/taskmaster +0 -21
- package/extensions/matrix/node_modules/.bin/markdown-it +0 -21
- package/extensions/matrix/node_modules/.bin/taskmaster +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
- package/extensions/msteams/node_modules/.bin/taskmaster +0 -21
- package/extensions/nostr/node_modules/.bin/taskmaster +0 -21
- package/extensions/nostr/node_modules/.bin/tsc +0 -21
- package/extensions/nostr/node_modules/.bin/tsserver +0 -21
- package/extensions/zalo/node_modules/.bin/taskmaster +0 -21
- package/extensions/zalouser/node_modules/.bin/taskmaster +0 -21
|
@@ -6,8 +6,8 @@
|
|
|
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BWqMMgRV.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B8I8lMfz.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
|
@@ -47,6 +47,7 @@ const BASE_RELOAD_RULES_TAIL = [
|
|
|
47
47
|
{ prefix: "plugins", kind: "restart" },
|
|
48
48
|
{ prefix: "ui", kind: "none" },
|
|
49
49
|
{ prefix: "workspaces", kind: "none" },
|
|
50
|
+
{ prefix: "business", kind: "none" },
|
|
50
51
|
{ prefix: "gateway.tailscale", kind: "none" },
|
|
51
52
|
{ prefix: "gateway", kind: "restart" },
|
|
52
53
|
{ prefix: "discovery", kind: "restart" },
|
|
@@ -80,6 +80,14 @@ export function createGatewayCloseHandler(params) {
|
|
|
80
80
|
/* ignore */
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
if (params.infraFallbackAlertUnsub) {
|
|
84
|
+
try {
|
|
85
|
+
params.infraFallbackAlertUnsub();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
83
91
|
params.chatRunState.clear();
|
|
84
92
|
for (const c of params.clients) {
|
|
85
93
|
try {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/config.js";
|
|
2
|
+
import { isPublicAgentActive } from "../../business/opening-hours.js";
|
|
3
|
+
export const businessHandlers = {
|
|
4
|
+
/**
|
|
5
|
+
* Return opening-hours config, publicAgentEnabled, and the combined open/closed status.
|
|
6
|
+
*/
|
|
7
|
+
"business.openingHours.get": ({ respond }) => {
|
|
8
|
+
const cfg = loadConfig();
|
|
9
|
+
const openingHours = cfg.business?.openingHours ?? null;
|
|
10
|
+
const publicAgentEnabled = cfg.business?.publicAgentEnabled !== false;
|
|
11
|
+
const status = isPublicAgentActive(cfg.business);
|
|
12
|
+
respond(true, {
|
|
13
|
+
openingHours,
|
|
14
|
+
publicAgentEnabled,
|
|
15
|
+
currentlyOpen: status.open,
|
|
16
|
+
reason: status.reason,
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* Convenience endpoint for setting opening hours.
|
|
21
|
+
* Delegates to config.patch — the zod schema validates the shape, and
|
|
22
|
+
* config.patch handles base-hash validation, persistence, and restart.
|
|
23
|
+
* The UI should call config.patch directly with the business.openingHours path.
|
|
24
|
+
*/
|
|
25
|
+
"business.openingHours.set": ({ respond }) => {
|
|
26
|
+
respond(true, {
|
|
27
|
+
ok: true,
|
|
28
|
+
note: "use config.patch with business.openingHours path",
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -17,6 +17,19 @@ import { ErrorCodes, errorShape } from "../protocol/index.js";
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Hostname helpers
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Extract a meaningful error message from an execFile failure.
|
|
22
|
+
* Node's execFile attaches `.stderr` to the thrown Error, which contains
|
|
23
|
+
* the actual diagnostic output (e.g. "Permission denied"). The `.message`
|
|
24
|
+
* property only has the generic "Command failed: ..." string.
|
|
25
|
+
*/
|
|
26
|
+
function execErrorDetail(err) {
|
|
27
|
+
if (err instanceof Error) {
|
|
28
|
+
const stderr = err.stderr?.trim();
|
|
29
|
+
return stderr || err.message;
|
|
30
|
+
}
|
|
31
|
+
return String(err);
|
|
32
|
+
}
|
|
20
33
|
/**
|
|
21
34
|
* Validate a hostname per RFC 1123: letters, digits, hyphens, 1–63 chars,
|
|
22
35
|
* cannot start or end with a hyphen.
|
|
@@ -67,12 +80,12 @@ async function setLocalHostname(hostname, log) {
|
|
|
67
80
|
const platform = os.platform();
|
|
68
81
|
if (platform === "darwin") {
|
|
69
82
|
try {
|
|
70
|
-
await runExec("/usr/sbin/scutil",
|
|
83
|
+
await runExec("sudo", ["/usr/sbin/scutil", "--set", "LocalHostName", hostname], {
|
|
71
84
|
timeoutMs: 10_000,
|
|
72
85
|
});
|
|
73
86
|
}
|
|
74
87
|
catch (err) {
|
|
75
|
-
const detail =
|
|
88
|
+
const detail = execErrorDetail(err);
|
|
76
89
|
log.warn(`network.setHostname: scutil failed: ${detail}`);
|
|
77
90
|
return `Failed to set hostname: ${detail}`;
|
|
78
91
|
}
|
|
@@ -80,20 +93,20 @@ async function setLocalHostname(hostname, log) {
|
|
|
80
93
|
}
|
|
81
94
|
if (platform === "linux") {
|
|
82
95
|
try {
|
|
83
|
-
await runExec("
|
|
96
|
+
await runExec("sudo", ["hostnamectl", "set-hostname", hostname], { timeoutMs: 10_000 });
|
|
84
97
|
}
|
|
85
98
|
catch (err) {
|
|
86
|
-
const detail =
|
|
99
|
+
const detail = execErrorDetail(err);
|
|
87
100
|
log.warn(`network.setHostname: hostnamectl failed: ${detail}`);
|
|
88
101
|
return `Failed to set hostname: ${detail}`;
|
|
89
102
|
}
|
|
90
103
|
// Restart avahi-daemon so mDNS advertises the new hostname.
|
|
91
104
|
try {
|
|
92
|
-
await runExec("
|
|
105
|
+
await runExec("sudo", ["systemctl", "restart", "avahi-daemon"], { timeoutMs: 10_000 });
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
|
95
108
|
// Non-fatal — hostname was set, but mDNS may take time to propagate.
|
|
96
|
-
const detail =
|
|
109
|
+
const detail = execErrorDetail(err);
|
|
97
110
|
log.warn(`network.setHostname: avahi-daemon restart failed: ${detail}`);
|
|
98
111
|
}
|
|
99
112
|
return null;
|
|
@@ -178,11 +178,27 @@ export const updateHandlers = {
|
|
|
178
178
|
durationMs: 0,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
+
const versionSummary = `${result.before?.version ?? "?"} → ${result.after?.version ?? "?"}`;
|
|
181
182
|
if (result.status === "ok") {
|
|
182
|
-
log.info(`software update completed (${result.mode}, ${result.durationMs}ms)`);
|
|
183
|
+
log.info(`software update completed: ${versionSummary} (${result.mode}, ${result.durationMs}ms)`);
|
|
183
184
|
}
|
|
184
185
|
else {
|
|
185
|
-
log.error(`software update ${result.status}: ${result.reason ?? "unknown"} (${result.mode}, ${result.durationMs}ms)`);
|
|
186
|
+
log.error(`software update ${result.status}: ${result.reason ?? "unknown"} [${versionSummary}] (${result.mode}, ${result.durationMs}ms)`);
|
|
187
|
+
}
|
|
188
|
+
// Log warnings from post-install verification
|
|
189
|
+
if (result.warnings?.length) {
|
|
190
|
+
for (const warning of result.warnings) {
|
|
191
|
+
log.error(`software update verification: ${warning}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Log step output for diagnostics (even on success)
|
|
195
|
+
for (const step of result.steps) {
|
|
196
|
+
if (step.stderrTail) {
|
|
197
|
+
log.info(`update step ${step.name} stderr:\n${step.stderrTail}`);
|
|
198
|
+
}
|
|
199
|
+
if (step.stdoutTail) {
|
|
200
|
+
log.info(`update step ${step.name} stdout:\n${step.stdoutTail}`);
|
|
201
|
+
}
|
|
186
202
|
}
|
|
187
203
|
const payload = {
|
|
188
204
|
kind: "update",
|
|
@@ -254,7 +270,8 @@ export const updateHandlers = {
|
|
|
254
270
|
// returns before our SIGTERM handler releases the port, causing the
|
|
255
271
|
// new process to fail with "port already in use" and crash-loop.
|
|
256
272
|
log.info("exiting for supervisor restart after global update");
|
|
257
|
-
|
|
273
|
+
// Allow a brief pause for async log buffers to flush before exit
|
|
274
|
+
setTimeout(() => process.exit(0), 500);
|
|
258
275
|
})();
|
|
259
276
|
}, delayMs);
|
|
260
277
|
restart = { ok: true };
|
|
@@ -39,6 +39,7 @@ import { networkHandlers } from "./server-methods/network.js";
|
|
|
39
39
|
import { wifiHandlers } from "./server-methods/wifi.js";
|
|
40
40
|
import { workspacesHandlers } from "./server-methods/workspaces.js";
|
|
41
41
|
import { brandHandlers } from "./server-methods/brand.js";
|
|
42
|
+
import { businessHandlers } from "./server-methods/business.js";
|
|
42
43
|
const ADMIN_SCOPE = "operator.admin";
|
|
43
44
|
const READ_SCOPE = "operator.read";
|
|
44
45
|
const WRITE_SCOPE = "operator.write";
|
|
@@ -107,6 +108,7 @@ const READ_METHODS = new Set([
|
|
|
107
108
|
"memory.status",
|
|
108
109
|
"memory.audit",
|
|
109
110
|
"qr.generate",
|
|
111
|
+
"business.openingHours.get",
|
|
110
112
|
]);
|
|
111
113
|
const WRITE_METHODS = new Set([
|
|
112
114
|
"send",
|
|
@@ -200,7 +202,8 @@ function authorizeGatewayMethod(method, client) {
|
|
|
200
202
|
method === "workspaces.remove" ||
|
|
201
203
|
method === "memory.reindex" ||
|
|
202
204
|
method === "memory.auditClear" ||
|
|
203
|
-
method === "system.uninstall"
|
|
205
|
+
method === "system.uninstall" ||
|
|
206
|
+
method === "business.openingHours.set") {
|
|
204
207
|
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
|
|
205
208
|
}
|
|
206
209
|
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
|
|
@@ -241,6 +244,7 @@ export const coreGatewayHandlers = {
|
|
|
241
244
|
...recordsHandlers,
|
|
242
245
|
...workspacesHandlers,
|
|
243
246
|
...brandHandlers,
|
|
247
|
+
...businessHandlers,
|
|
244
248
|
...publicChatHandlers,
|
|
245
249
|
...qrHandlers,
|
|
246
250
|
...networkHandlers,
|
|
@@ -16,6 +16,9 @@ import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
|
|
|
16
16
|
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
|
|
17
17
|
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
|
|
18
18
|
import { onInfraAlertEvent } from "../infra/infra-alert-events.js";
|
|
19
|
+
import { sendCategorizedInfraAlert } from "../infra/heartbeat-infra-alert.js";
|
|
20
|
+
import { resolveAgentBoundAccountId } from "../routing/bindings.js";
|
|
21
|
+
import { resolveHeartbeatDeliveryTarget } from "../infra/outbound/targets.js";
|
|
19
22
|
import { getMachineDisplayName } from "../infra/machine-name.js";
|
|
20
23
|
import { ensureTaskmasterCliOnPath } from "../infra/path-env.js";
|
|
21
24
|
import { primeRemoteSkillsCache, refreshRemoteBinsForConnectedNodes, setSkillsRemoteRegistry, } from "../infra/skills-remote.js";
|
|
@@ -178,6 +181,17 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
178
181
|
log.warn(`gateway: failed to update config version stamp: ${String(err)}`);
|
|
179
182
|
}
|
|
180
183
|
}
|
|
184
|
+
// One-time consolidation of per-agent auth stores into the global store.
|
|
185
|
+
try {
|
|
186
|
+
const { consolidateAuthProfileStores } = await import("../agents/auth-profiles/consolidate.js");
|
|
187
|
+
const authChanges = consolidateAuthProfileStores();
|
|
188
|
+
if (authChanges.length > 0) {
|
|
189
|
+
log.info(`Auth store consolidation:\n${authChanges.map((c) => ` - ${c}`).join("\n")}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log.warn(`Auth store consolidation failed: ${String(err)}`);
|
|
194
|
+
}
|
|
181
195
|
const cfgAtStart = loadConfig();
|
|
182
196
|
// License check — gateway always starts (so setup UI is reachable),
|
|
183
197
|
// but pages other than /setup will redirect until a license is activated.
|
|
@@ -397,6 +411,33 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
397
411
|
const infraAlertUnsub = onInfraAlertEvent((evt) => {
|
|
398
412
|
broadcast("notification", evt);
|
|
399
413
|
});
|
|
414
|
+
const infraFallbackAlertUnsub = onInfraAlertEvent(async (evt) => {
|
|
415
|
+
if (evt.category !== "model-fallback")
|
|
416
|
+
return;
|
|
417
|
+
try {
|
|
418
|
+
const cfg = loadConfig();
|
|
419
|
+
const defaultAgentId = resolveDefaultAgentId(cfg);
|
|
420
|
+
const agent = cfg.agents?.list?.find((a) => a.id === defaultAgentId);
|
|
421
|
+
const heartbeat = agent?.heartbeat ?? cfg.agents?.defaults?.heartbeat;
|
|
422
|
+
const bindingAccountId = resolveAgentBoundAccountId(cfg, defaultAgentId, "whatsapp") ?? undefined;
|
|
423
|
+
const delivery = resolveHeartbeatDeliveryTarget({
|
|
424
|
+
cfg,
|
|
425
|
+
entry: undefined,
|
|
426
|
+
heartbeat,
|
|
427
|
+
bindingAccountId,
|
|
428
|
+
});
|
|
429
|
+
if (delivery.channel === "none" || !delivery.to)
|
|
430
|
+
return;
|
|
431
|
+
await sendCategorizedInfraAlert({
|
|
432
|
+
cfg,
|
|
433
|
+
delivery,
|
|
434
|
+
category: "model-fallback",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Best-effort — never crash the gateway
|
|
439
|
+
}
|
|
440
|
+
});
|
|
400
441
|
let heartbeatRunner = startHeartbeatRunner({ cfg: cfgAtStart });
|
|
401
442
|
// Start cron, then seed preloaded cron jobs from bundled skills (one-time per template).
|
|
402
443
|
void cron
|
|
@@ -589,6 +630,7 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
589
630
|
agentUnsub,
|
|
590
631
|
heartbeatUnsub,
|
|
591
632
|
infraAlertUnsub,
|
|
633
|
+
infraFallbackAlertUnsub,
|
|
592
634
|
chatRunState,
|
|
593
635
|
clients,
|
|
594
636
|
configReloader,
|
|
@@ -10,6 +10,7 @@ const MESSAGES = {
|
|
|
10
10
|
auth: "Your AI assistant is offline because the API key is invalid or has expired. Open the control panel and go to Settings > API Keys to update it.",
|
|
11
11
|
billing: "Your AI assistant is offline because of a billing issue with the AI provider. Check your AI provider account.",
|
|
12
12
|
repeated: (n) => `Your AI assistant has failed to respond ${n} times in a row.`,
|
|
13
|
+
"model-fallback": "Your AI assistant's primary model failed and is using a backup model. Check the control panel for details or re-authenticate your primary provider.",
|
|
13
14
|
};
|
|
14
15
|
// In-memory cooldown map: category → last-sent timestamp.
|
|
15
16
|
// Resets on gateway restart — correct: admin may have restarted to fix the issue.
|
|
@@ -135,6 +136,59 @@ export async function maybeAlertAdmin(ctx) {
|
|
|
135
136
|
return false;
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Send an infra alert for a specific category with cooldown.
|
|
141
|
+
* Extracts the common delivery logic from maybeAlertAdmin so it can be
|
|
142
|
+
* reused for event-driven alerts (e.g. model-fallback).
|
|
143
|
+
*
|
|
144
|
+
* Returns true if an alert was sent, false if suppressed or skipped.
|
|
145
|
+
* Never throws.
|
|
146
|
+
*/
|
|
147
|
+
export async function sendCategorizedInfraAlert(ctx) {
|
|
148
|
+
try {
|
|
149
|
+
const { cfg, delivery, category, deps } = ctx;
|
|
150
|
+
const nowMs = ctx.nowMs ?? Date.now();
|
|
151
|
+
if (delivery.channel === "none" || !delivery.to)
|
|
152
|
+
return false;
|
|
153
|
+
if (isCooledDown(category, nowMs)) {
|
|
154
|
+
log.debug("categorized infra alert suppressed (cooldown)", { category });
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const plugin = getChannelPlugin(delivery.channel);
|
|
158
|
+
if (plugin?.heartbeat?.checkReady) {
|
|
159
|
+
const readiness = await plugin.heartbeat.checkReady({
|
|
160
|
+
cfg,
|
|
161
|
+
accountId: delivery.accountId,
|
|
162
|
+
deps,
|
|
163
|
+
});
|
|
164
|
+
if (!readiness.ok) {
|
|
165
|
+
log.info("categorized infra alert skipped: channel not ready", {
|
|
166
|
+
category,
|
|
167
|
+
reason: readiness.reason,
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const message = ctx.message ?? resolveAlertMessage(category);
|
|
173
|
+
await deliverOutboundPayloads({
|
|
174
|
+
cfg,
|
|
175
|
+
channel: delivery.channel,
|
|
176
|
+
to: delivery.to,
|
|
177
|
+
accountId: delivery.accountId,
|
|
178
|
+
payloads: [{ text: message }],
|
|
179
|
+
deps,
|
|
180
|
+
});
|
|
181
|
+
cooldowns.set(category, nowMs);
|
|
182
|
+
log.info("categorized infra alert sent", { category, to: delivery.to });
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch (alertErr) {
|
|
186
|
+
log.error("categorized infra alert delivery failed", {
|
|
187
|
+
error: alertErr instanceof Error ? alertErr.message : String(alertErr),
|
|
188
|
+
});
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
138
192
|
/** Reset consecutive unknown failure counter. Call when heartbeat succeeds. */
|
|
139
193
|
export function resetConsecutiveFailures() {
|
|
140
194
|
consecutiveUnknownFailures = 0;
|
|
@@ -464,15 +464,40 @@ export async function runGatewayUpdate(opts = {}) {
|
|
|
464
464
|
});
|
|
465
465
|
const steps = [updateStep];
|
|
466
466
|
const afterVersion = await readPackageVersion(pkgRoot);
|
|
467
|
+
// Post-install verification: check for issues even when exit code is 0
|
|
468
|
+
const warnings = [];
|
|
469
|
+
let versionUnchanged = false;
|
|
470
|
+
if (updateStep.exitCode === 0) {
|
|
471
|
+
// Verify version actually changed
|
|
472
|
+
if (beforeVersion && afterVersion && beforeVersion === afterVersion) {
|
|
473
|
+
versionUnchanged = true;
|
|
474
|
+
warnings.push(`version unchanged after install: ${beforeVersion} → ${afterVersion}`);
|
|
475
|
+
}
|
|
476
|
+
// Verify extensions directory exists (critical for plugin loading)
|
|
477
|
+
const extensionsDir = path.join(pkgRoot, "extensions");
|
|
478
|
+
try {
|
|
479
|
+
await fs.access(extensionsDir);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
warnings.push(`extensions directory missing after install: ${extensionsDir}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const status = updateStep.exitCode !== 0 ? "error" : versionUnchanged ? "error" : "ok";
|
|
486
|
+
const reason = updateStep.exitCode !== 0
|
|
487
|
+
? updateStep.name
|
|
488
|
+
: versionUnchanged
|
|
489
|
+
? `version unchanged after install: ${beforeVersion}`
|
|
490
|
+
: undefined;
|
|
467
491
|
return {
|
|
468
|
-
status
|
|
492
|
+
status,
|
|
469
493
|
mode: globalManager,
|
|
470
494
|
root: pkgRoot,
|
|
471
|
-
reason
|
|
495
|
+
reason,
|
|
472
496
|
before: { version: beforeVersion },
|
|
473
497
|
after: { version: afterVersion },
|
|
474
498
|
steps,
|
|
475
499
|
durationMs: Date.now() - startedAt,
|
|
500
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
476
501
|
};
|
|
477
502
|
}
|
|
478
503
|
return {
|
package/dist/memory/manager.js
CHANGED
|
@@ -314,7 +314,7 @@ export class MemoryIndexManager {
|
|
|
314
314
|
const effectiveProvider = providerResult.fallbackFrom
|
|
315
315
|
? `${providerResult.fallbackFrom} → ${providerResult.provider.id} (fallback)`
|
|
316
316
|
: providerResult.provider.id;
|
|
317
|
-
log.
|
|
317
|
+
log.debug(`embeddings: using ${effectiveProvider} provider`, {
|
|
318
318
|
model: providerResult.provider.model,
|
|
319
319
|
agentId,
|
|
320
320
|
});
|
|
@@ -924,7 +924,7 @@ export class MemoryIndexManager {
|
|
|
924
924
|
}
|
|
925
925
|
}
|
|
926
926
|
if (orphans.length > 0) {
|
|
927
|
-
log.
|
|
927
|
+
log.debug(`cleaned up ${orphans.length} orphaned temp index file(s)`);
|
|
928
928
|
}
|
|
929
929
|
}
|
|
930
930
|
moveIndexFilesSync(sourceBase, targetBase) {
|
|
@@ -1279,7 +1279,7 @@ export class MemoryIndexManager {
|
|
|
1279
1279
|
return;
|
|
1280
1280
|
}
|
|
1281
1281
|
const action = record ? "updated" : "added";
|
|
1282
|
-
log.
|
|
1282
|
+
log.debug(`file ${action} (${this.agentId}): ${entry.path}`);
|
|
1283
1283
|
if (isAuditablePath(entry.path) && record?.hash !== entry.hash) {
|
|
1284
1284
|
recordAuditEntry(this.workspaceDir, {
|
|
1285
1285
|
path: entry.path,
|
|
@@ -1304,7 +1304,7 @@ export class MemoryIndexManager {
|
|
|
1304
1304
|
for (const stale of staleRows) {
|
|
1305
1305
|
if (activePaths.has(stale.path))
|
|
1306
1306
|
continue;
|
|
1307
|
-
log.
|
|
1307
|
+
log.debug(`file deleted (${this.agentId}): ${stale.path}`);
|
|
1308
1308
|
this.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory");
|
|
1309
1309
|
try {
|
|
1310
1310
|
this.db
|
|
@@ -1463,7 +1463,7 @@ export class MemoryIndexManager {
|
|
|
1463
1463
|
if (!needsFullReindex && this.sources.has("memory") && !this.dirty) {
|
|
1464
1464
|
const memoryFileCount = this.db.prepare("SELECT COUNT(*) as c FROM files WHERE source = ?").get("memory").c;
|
|
1465
1465
|
if (memoryFileCount === 0) {
|
|
1466
|
-
log.
|
|
1466
|
+
log.debug("memory source enabled but no memory files indexed — marking dirty");
|
|
1467
1467
|
this.dirty = true;
|
|
1468
1468
|
}
|
|
1469
1469
|
}
|
|
@@ -19,6 +19,7 @@ import { deliverWebReply } from "../deliver-reply.js";
|
|
|
19
19
|
import { whatsappInboundLog, whatsappOutboundLog } from "../loggers.js";
|
|
20
20
|
import { elide } from "../util.js";
|
|
21
21
|
import { createInternalHookEvent, triggerInternalHook } from "../../../hooks/internal-hooks.js";
|
|
22
|
+
import { appendUserMessageToSessionTranscript } from "../../../config/sessions/transcript.js";
|
|
22
23
|
import { maybeSendAckReaction } from "./ack-reaction.js";
|
|
23
24
|
import { formatGroupMembers } from "./group-members.js";
|
|
24
25
|
import { trackBackgroundTask, updateLastRouteInBackground } from "./last-route.js";
|
|
@@ -156,6 +157,29 @@ export async function processMessage(params) {
|
|
|
156
157
|
if (!hasMediaAttachment) {
|
|
157
158
|
fireInboundArchiveHook(params.msg.body);
|
|
158
159
|
}
|
|
160
|
+
// ── Opening hours gate ──────────────────────────────────────────────
|
|
161
|
+
// Check if the business is currently closed. Only gate the public agent.
|
|
162
|
+
const agentEntries = listAgentEntries(params.cfg);
|
|
163
|
+
const routedAgent = agentEntries.find((a) => a.id === params.route.agentId);
|
|
164
|
+
const isPublicAgent = routedAgent && !routedAgent.default && routedAgent.tools?.profile !== "full";
|
|
165
|
+
if (isPublicAgent && params.msg.businessClosed) {
|
|
166
|
+
params.replyLogger.info({
|
|
167
|
+
from: params.msg.from,
|
|
168
|
+
agentId: params.route.agentId,
|
|
169
|
+
reason: "business closed",
|
|
170
|
+
}, "opening hours gate: business closed, skipping agent dispatch");
|
|
171
|
+
// Store the user turn in the session so the agent has context when business reopens.
|
|
172
|
+
void appendUserMessageToSessionTranscript({
|
|
173
|
+
agentId: params.route.agentId,
|
|
174
|
+
sessionKey: params.route.sessionKey,
|
|
175
|
+
text: combinedBody,
|
|
176
|
+
storePath,
|
|
177
|
+
}).catch((err) => {
|
|
178
|
+
params.replyLogger.warn({ error: formatError(err) }, "opening hours gate: failed to store user turn");
|
|
179
|
+
});
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// ── End opening hours gate ──────────────────────────────────────────
|
|
159
183
|
// Send ack reaction immediately upon message receipt (post-gating).
|
|
160
184
|
// Suppress when running silently on un-mentioned group messages.
|
|
161
185
|
if (params.suppressDelivery) {
|
|
@@ -77,12 +77,13 @@ export async function checkInboundAccessControl(params) {
|
|
|
77
77
|
// DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled".
|
|
78
78
|
if (!params.group) {
|
|
79
79
|
if (params.isFromMe && !isSamePhone) {
|
|
80
|
-
logVerbose("
|
|
80
|
+
logVerbose("Detected outbound DM (fromMe); flagging for session mirroring.");
|
|
81
81
|
return {
|
|
82
82
|
allowed: false,
|
|
83
83
|
shouldMarkRead: false,
|
|
84
84
|
isSelfChat,
|
|
85
85
|
resolvedAccountId: account.accountId,
|
|
86
|
+
isOwnerOutbound: true,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
// Admin phones (explicit peer binding for this account) are always allowed,
|
|
@@ -5,6 +5,8 @@ import { recordChannelActivity } from "../../infra/channel-activity.js";
|
|
|
5
5
|
import { getChildLogger } from "../../logging/logger.js";
|
|
6
6
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
7
7
|
import { saveMediaBuffer } from "../../media/store.js";
|
|
8
|
+
import { isPublicAgentActive } from "../../business/opening-hours.js";
|
|
9
|
+
import { loadConfig } from "../../config/config.js";
|
|
8
10
|
import { createInboundDebouncer } from "../../auto-reply/inbound-debounce.js";
|
|
9
11
|
import { resolveJidToE164 } from "../../utils.js";
|
|
10
12
|
import { createWaSocket, getStatusCode, waitForWaConnection } from "../session.js";
|
|
@@ -12,6 +14,7 @@ import { checkInboundAccessControl } from "./access-control.js";
|
|
|
12
14
|
import { isRecentInboundMessage } from "./dedupe.js";
|
|
13
15
|
import { describeReplyContext, extractLocationData, extractMediaPlaceholder, extractMentionedJids, extractText, } from "./extract.js";
|
|
14
16
|
import { downloadInboundMedia } from "./media.js";
|
|
17
|
+
import { mirrorOwnerReplyToSession } from "./owner-mirror.js";
|
|
15
18
|
import { createWebSendApi } from "./send-api.js";
|
|
16
19
|
export async function monitorWebInbox(options) {
|
|
17
20
|
const inboundLogger = getChildLogger({ module: "web-inbound" });
|
|
@@ -117,7 +120,7 @@ export async function monitorWebInbox(options) {
|
|
|
117
120
|
}
|
|
118
121
|
};
|
|
119
122
|
const handleMessagesUpsert = async (upsert) => {
|
|
120
|
-
inboundLogger.
|
|
123
|
+
inboundLogger.debug({ accountId: options.accountId, type: upsert.type, count: upsert.messages?.length ?? 0 }, "messages.upsert received");
|
|
121
124
|
if (upsert.type !== "notify" && upsert.type !== "append") {
|
|
122
125
|
inboundLogger.warn({ type: upsert.type }, "messages.upsert dropped — unexpected type");
|
|
123
126
|
return;
|
|
@@ -125,7 +128,7 @@ export async function monitorWebInbox(options) {
|
|
|
125
128
|
for (const msg of upsert.messages ?? []) {
|
|
126
129
|
const id = msg.key?.id ?? undefined;
|
|
127
130
|
const remoteJid = msg.key?.remoteJid;
|
|
128
|
-
inboundLogger.
|
|
131
|
+
inboundLogger.debug({
|
|
129
132
|
accountId: options.accountId,
|
|
130
133
|
id,
|
|
131
134
|
remoteJid,
|
|
@@ -142,14 +145,14 @@ export async function monitorWebInbox(options) {
|
|
|
142
145
|
continue;
|
|
143
146
|
}
|
|
144
147
|
if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) {
|
|
145
|
-
inboundLogger.
|
|
148
|
+
inboundLogger.debug({ id, remoteJid }, "dropped — status/broadcast");
|
|
146
149
|
continue;
|
|
147
150
|
}
|
|
148
151
|
const group = isJidGroup(remoteJid) === true;
|
|
149
152
|
if (id) {
|
|
150
153
|
const dedupeKey = `${options.accountId}:${remoteJid}:${id}`;
|
|
151
154
|
if (isRecentInboundMessage(dedupeKey)) {
|
|
152
|
-
inboundLogger.
|
|
155
|
+
inboundLogger.debug({ id, remoteJid }, "dropped — dedupe");
|
|
153
156
|
continue;
|
|
154
157
|
}
|
|
155
158
|
}
|
|
@@ -188,11 +191,29 @@ export async function monitorWebInbox(options) {
|
|
|
188
191
|
remoteJid,
|
|
189
192
|
});
|
|
190
193
|
if (!access.allowed) {
|
|
191
|
-
|
|
194
|
+
if (access.isOwnerOutbound) {
|
|
195
|
+
// Owner sent a DM to a customer — mirror into the public agent's session
|
|
196
|
+
// so the agent is aware of the intervention.
|
|
197
|
+
const body = extractText(msg.message ?? undefined);
|
|
198
|
+
if (body) {
|
|
199
|
+
void mirrorOwnerReplyToSession({
|
|
200
|
+
cfg: loadConfig(),
|
|
201
|
+
from,
|
|
202
|
+
body,
|
|
203
|
+
accountId: access.resolvedAccountId,
|
|
204
|
+
timestamp: messageTimestampMs,
|
|
205
|
+
logger: inboundLogger,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
inboundLogger.debug({ id, from }, "dropped — access denied");
|
|
211
|
+
}
|
|
192
212
|
continue;
|
|
193
213
|
}
|
|
194
|
-
inboundLogger.
|
|
195
|
-
|
|
214
|
+
inboundLogger.debug({ id, from, isSelfChat: access.isSelfChat }, "access granted");
|
|
215
|
+
const businessClosed = !isPublicAgentActive(loadConfig().business).open;
|
|
216
|
+
if (id && !access.isSelfChat && options.sendReadReceipts !== false && !businessClosed) {
|
|
196
217
|
const participant = msg.key?.participant;
|
|
197
218
|
try {
|
|
198
219
|
await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]);
|
|
@@ -211,7 +232,7 @@ export async function monitorWebInbox(options) {
|
|
|
211
232
|
}
|
|
212
233
|
// If this is history/offline catch-up, mark read above but skip auto-reply.
|
|
213
234
|
if (upsert.type === "append") {
|
|
214
|
-
inboundLogger.
|
|
235
|
+
inboundLogger.debug({ id, from }, "dropped — append (history catch-up)");
|
|
215
236
|
continue;
|
|
216
237
|
}
|
|
217
238
|
const location = extractLocationData(msg.message ?? undefined);
|
|
@@ -223,7 +244,7 @@ export async function monitorWebInbox(options) {
|
|
|
223
244
|
if (!body) {
|
|
224
245
|
body = extractMediaPlaceholder(msg.message ?? undefined);
|
|
225
246
|
if (!body) {
|
|
226
|
-
inboundLogger.
|
|
247
|
+
inboundLogger.debug({ id, from }, "dropped — no body or media placeholder");
|
|
227
248
|
continue;
|
|
228
249
|
}
|
|
229
250
|
}
|
|
@@ -270,7 +291,7 @@ export async function monitorWebInbox(options) {
|
|
|
270
291
|
const timestamp = messageTimestampMs;
|
|
271
292
|
const mentionedJids = extractMentionedJids(msg.message);
|
|
272
293
|
const senderName = msg.pushName ?? undefined;
|
|
273
|
-
inboundLogger.
|
|
294
|
+
inboundLogger.debug({ from, to: selfE164 ?? "me", body, mediaPath, mediaType, timestamp }, "inbound message");
|
|
274
295
|
const inboundMessage = {
|
|
275
296
|
id,
|
|
276
297
|
from,
|
|
@@ -302,6 +323,7 @@ export async function monitorWebInbox(options) {
|
|
|
302
323
|
mediaPath,
|
|
303
324
|
mediaType,
|
|
304
325
|
mediaDownloadFailed,
|
|
326
|
+
businessClosed,
|
|
305
327
|
};
|
|
306
328
|
try {
|
|
307
329
|
const task = Promise.resolve(debouncer.enqueue(inboundMessage));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveStorePath } from "../../config/sessions.js";
|
|
2
|
+
import { appendAssistantMessageToSessionTranscript } from "../../config/sessions/transcript.js";
|
|
3
|
+
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
|
4
|
+
export async function mirrorOwnerReplyToSession(params) {
|
|
5
|
+
try {
|
|
6
|
+
// Resolve the session key for this customer's conversation.
|
|
7
|
+
// The 'from' is the customer's phone number (remoteJid resolved to E.164).
|
|
8
|
+
const route = resolveAgentRoute({
|
|
9
|
+
cfg: params.cfg,
|
|
10
|
+
channel: "whatsapp",
|
|
11
|
+
accountId: params.accountId,
|
|
12
|
+
peer: {
|
|
13
|
+
kind: "dm",
|
|
14
|
+
id: params.from,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!route.agentId)
|
|
18
|
+
return;
|
|
19
|
+
const storePath = resolveStorePath(params.cfg.session?.store, {
|
|
20
|
+
agentId: route.agentId,
|
|
21
|
+
});
|
|
22
|
+
const result = await appendAssistantMessageToSessionTranscript({
|
|
23
|
+
agentId: route.agentId,
|
|
24
|
+
sessionKey: route.sessionKey,
|
|
25
|
+
text: `[Owner reply] ${params.body}`,
|
|
26
|
+
storePath,
|
|
27
|
+
});
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
params.logger.info({ from: params.from, agentId: route.agentId, sessionKey: route.sessionKey }, "mirrored owner outbound DM to session");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
params.logger.warn({ error: String(err), from: params.from }, "failed to mirror owner outbound DM to session");
|
|
34
|
+
}
|
|
35
|
+
}
|