@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.
Files changed (115) hide show
  1. package/dist/agents/auth-profiles/consolidate.js +72 -0
  2. package/dist/agents/auth-profiles/oauth.js +0 -24
  3. package/dist/agents/auth-profiles/paths.js +4 -4
  4. package/dist/agents/auth-profiles/store.js +8 -100
  5. package/dist/agents/model-fallback.js +26 -1
  6. package/dist/agents/pi-embedded-runner/run/payloads.js +8 -0
  7. package/dist/agents/session-transcript-repair.js +3 -2
  8. package/dist/agents/system-prompt.js +1 -0
  9. package/dist/agents/taskmaster-tools.js +2 -0
  10. package/dist/agents/tool-policy.js +2 -0
  11. package/dist/agents/tools/opening-hours-tool.js +92 -0
  12. package/dist/agents/tools/web-fetch.js +8 -3
  13. package/dist/agents/tools/web-search.js +7 -4
  14. package/dist/agents/workspace-migrations.js +47 -0
  15. package/dist/build-info.json +3 -3
  16. package/dist/commands/agents.commands.add.js +1 -32
  17. package/dist/config/defaults.js +1 -1
  18. package/dist/config/legacy.migrations.part-3.js +25 -4
  19. package/dist/config/sessions/transcript.js +31 -0
  20. package/dist/config/types.business.js +1 -0
  21. package/dist/config/zod-schema.js +33 -0
  22. package/dist/control-ui/assets/{index-CpaEIgQy.css → index-B8I8lMfz.css} +1 -1
  23. package/dist/control-ui/assets/{index-CP9IoaZp.js → index-BWqMMgRV.js} +537 -425
  24. package/dist/control-ui/assets/index-BWqMMgRV.js.map +1 -0
  25. package/dist/control-ui/index.html +2 -2
  26. package/dist/gateway/config-reload.js +1 -0
  27. package/dist/gateway/server-close.js +8 -0
  28. package/dist/gateway/server-methods/business.js +31 -0
  29. package/dist/gateway/server-methods/network.js +19 -6
  30. package/dist/gateway/server-methods/update.js +20 -3
  31. package/dist/gateway/server-methods.js +5 -1
  32. package/dist/gateway/server.impl.js +42 -0
  33. package/dist/infra/heartbeat-infra-alert.js +54 -0
  34. package/dist/infra/update-runner.js +27 -2
  35. package/dist/memory/manager.js +5 -5
  36. package/dist/web/auto-reply/monitor/process-message.js +24 -0
  37. package/dist/web/inbound/access-control.js +2 -1
  38. package/dist/web/inbound/monitor.js +32 -10
  39. package/dist/web/inbound/owner-mirror.js +35 -0
  40. package/package.json +1 -1
  41. package/skills/anthropic/SKILL.md +30 -0
  42. package/skills/anthropic/references/setup-guide.md +146 -0
  43. package/skills/google-ai/SKILL.md +3 -2
  44. package/skills/google-ai/references/setup-guide.md +94 -0
  45. package/skills/log-review/SKILL.md +45 -0
  46. package/skills/log-review/cron-template.json +21 -0
  47. package/skills/log-review/references/review-protocol.md +65 -0
  48. package/skills/openai/SKILL.md +28 -0
  49. package/skills/openai/references/setup-guide.md +122 -0
  50. package/taskmaster-docs/USER-GUIDE.md +31 -2
  51. package/templates/beagle-taxi/memory/public/investors-knowledge-base.md +230 -0
  52. package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +3 -1
  53. package/templates/customer/agents/admin/BOOTSTRAP.md +14 -2
  54. package/templates/customer/agents/public/AGENTS.md +15 -0
  55. package/templates/education-hero/agents/admin/BOOTSTRAP.md +14 -2
  56. package/templates/real-agent/agents/admin/AGENTS.md +139 -0
  57. package/templates/real-agent/agents/admin/HEARTBEAT.md +12 -0
  58. package/templates/real-agent/agents/admin/IDENTITY.md +11 -0
  59. package/templates/real-agent/agents/admin/SOUL.md +38 -0
  60. package/templates/real-agent/agents/public/AGENTS.md +183 -0
  61. package/templates/real-agent/agents/public/IDENTITY.md +8 -0
  62. package/templates/real-agent/agents/public/SOUL.md +75 -0
  63. package/templates/real-agent/memory/admin/.gitkeep +0 -0
  64. package/templates/real-agent/memory/public/contributors/adam-mackay.md +7 -0
  65. package/templates/real-agent/memory/public/contributors/alex-pelosi-buchanan.md +7 -0
  66. package/templates/real-agent/memory/public/contributors/jamie-fisher.md +7 -0
  67. package/templates/real-agent/memory/public/contributors/john-savage.md +7 -0
  68. package/templates/real-agent/memory/public/contributors/melanie-attwater.md +7 -0
  69. package/templates/real-agent/memory/public/contributors/regina-mangan.md +7 -0
  70. package/templates/real-agent/memory/public/contributors/richard-rawlings.md +7 -0
  71. package/templates/real-agent/memory/public/contributors/roger-black.md +7 -0
  72. package/templates/real-agent/memory/public/contributors/steve-backley.md +7 -0
  73. package/templates/real-agent/memory/public/courses/agency-blueprint/.gitkeep +0 -0
  74. package/templates/real-agent/memory/public/courses/podcast/.gitkeep +0 -0
  75. package/templates/real-agent/memory/public/courses/real-business/.gitkeep +0 -0
  76. package/templates/real-agent/memory/public/courses/real-coaching/.gitkeep +0 -0
  77. package/templates/real-agent/memory/public/courses/real-marketing/.gitkeep +0 -0
  78. package/templates/real-agent/memory/public/resources/.gitkeep +0 -0
  79. package/templates/real-agent/memory/shared/.gitkeep +0 -0
  80. package/templates/real-agent/memory/users/.gitkeep +0 -0
  81. package/templates/real-agent/skills/bespoke-coaching/SKILL.md +29 -0
  82. package/templates/real-agent/skills/bespoke-coaching/references/coaching-boundaries.md +56 -0
  83. package/templates/real-agent/skills/bespoke-coaching/references/feedback-framework.md +61 -0
  84. package/templates/real-agent/skills/bootstrap/SKILL.md +27 -0
  85. package/templates/real-agent/skills/bootstrap/references/onboarding-flow.md +63 -0
  86. package/templates/real-agent/skills/content-directory/SKILL.md +40 -0
  87. package/templates/real-agent/skills/content-directory/references/module-delivery.md +65 -0
  88. package/templates/real-agent/skills/content-directory/references/progress-tracking.md +47 -0
  89. package/templates/tradesupport/agents/admin/BOOTSTRAP.md +14 -2
  90. package/templates/zanzi-taxi/agents/admin/AGENTS.md +58 -0
  91. package/templates/zanzi-taxi/agents/admin/HEARTBEAT.md +12 -0
  92. package/templates/zanzi-taxi/agents/admin/IDENTITY.md +9 -0
  93. package/templates/zanzi-taxi/agents/admin/SOUL.md +33 -0
  94. package/templates/zanzi-taxi/agents/public/AGENTS.md +71 -0
  95. package/templates/zanzi-taxi/agents/public/IDENTITY.md +8 -0
  96. package/templates/zanzi-taxi/agents/public/SOUL.md +58 -0
  97. package/templates/zanzi-taxi/memory/public/knowledge-base.md +156 -0
  98. package/templates/zanzi-taxi/skills/zanzi-taxi/SKILL.md +39 -0
  99. package/templates/zanzi-taxi/skills/zanzi-taxi/references/local-knowledge.md +32 -0
  100. package/templates/zanzi-taxi/skills/zanzi-taxi/references/post-ride.md +42 -0
  101. package/templates/zanzi-taxi/skills/zanzi-taxi/references/ride-matching.md +74 -0
  102. package/dist/control-ui/assets/index-CP9IoaZp.js.map +0 -1
  103. package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -21
  104. package/extensions/googlechat/node_modules/.bin/taskmaster +0 -21
  105. package/extensions/line/node_modules/.bin/taskmaster +0 -21
  106. package/extensions/matrix/node_modules/.bin/markdown-it +0 -21
  107. package/extensions/matrix/node_modules/.bin/taskmaster +0 -21
  108. package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -21
  109. package/extensions/memory-lancedb/node_modules/.bin/openai +0 -21
  110. package/extensions/msteams/node_modules/.bin/taskmaster +0 -21
  111. package/extensions/nostr/node_modules/.bin/taskmaster +0 -21
  112. package/extensions/nostr/node_modules/.bin/tsc +0 -21
  113. package/extensions/nostr/node_modules/.bin/tsserver +0 -21
  114. package/extensions/zalo/node_modules/.bin/taskmaster +0 -21
  115. 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-CP9IoaZp.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-CpaEIgQy.css">
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", ["--set", "LocalHostName", hostname], {
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 = err instanceof Error ? err.message : String(err);
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("hostnamectl", ["set-hostname", hostname], { timeoutMs: 10_000 });
96
+ await runExec("sudo", ["hostnamectl", "set-hostname", hostname], { timeoutMs: 10_000 });
84
97
  }
85
98
  catch (err) {
86
- const detail = err instanceof Error ? err.message : String(err);
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("systemctl", ["restart", "avahi-daemon"], { timeoutMs: 10_000 });
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 = err instanceof Error ? err.message : String(err);
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
- process.exit(0);
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: updateStep.exitCode === 0 ? "ok" : "error",
492
+ status,
469
493
  mode: globalManager,
470
494
  root: pkgRoot,
471
- reason: updateStep.exitCode === 0 ? undefined : updateStep.name,
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 {
@@ -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.info(`embeddings: using ${effectiveProvider} provider`, {
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.info(`cleaned up ${orphans.length} orphaned temp index file(s)`);
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.info(`file ${action} (${this.agentId}): ${entry.path}`);
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.info(`file deleted (${this.agentId}): ${stale.path}`);
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.info("memory source enabled but no memory files indexed — marking dirty");
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("Skipping outbound DM (fromMe); no pairing reply needed.");
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.info({ accountId: options.accountId, type: upsert.type, count: upsert.messages?.length ?? 0 }, "messages.upsert received");
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.info({
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.info({ id, remoteJid }, "dropped — status/broadcast");
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.info({ id, remoteJid }, "dropped — dedupe");
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
- inboundLogger.info({ id, from }, "dropped — access denied");
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.info({ id, from, isSelfChat: access.isSelfChat }, "access granted");
195
- if (id && !access.isSelfChat && options.sendReadReceipts !== false) {
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.info({ id, from }, "dropped — append (history catch-up)");
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.info({ id, from }, "dropped — no body or media placeholder");
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.info({ from, to: selfE164 ?? "me", body, mediaPath, mediaType, timestamp }, "inbound message");
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.12.3",
3
+ "version": "1.13.1",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"