@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
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveStateDir } from "../../config/paths.js";
4
+ import { AUTH_PROFILE_FILENAME } from "./constants.js";
5
+ import { resolveAuthStorePath } from "./paths.js";
6
+ import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
7
+ /**
8
+ * One-time migration: scan per-agent auth-profiles.json files, merge any
9
+ * fresher OAuth tokens into the global store, then rename per-agent files
10
+ * to `.bak`.
11
+ *
12
+ * Returns a list of human-readable change descriptions (empty if no work done).
13
+ */
14
+ export function consolidateAuthProfileStores() {
15
+ const changes = [];
16
+ const mainPath = resolveAuthStorePath();
17
+ const mainStore = ensureAuthProfileStore();
18
+ const agentsDir = path.join(resolveStateDir(), "agents");
19
+ let agentDirs;
20
+ try {
21
+ agentDirs = fs.readdirSync(agentsDir);
22
+ }
23
+ catch {
24
+ return changes;
25
+ }
26
+ for (const dir of agentDirs) {
27
+ const agentAuthPath = path.join(agentsDir, dir, "agent", AUTH_PROFILE_FILENAME);
28
+ // Skip the main agent's store — it IS the global store.
29
+ if (path.resolve(agentAuthPath) === path.resolve(mainPath))
30
+ continue;
31
+ let raw;
32
+ try {
33
+ raw = fs.readFileSync(agentAuthPath, "utf8");
34
+ }
35
+ catch {
36
+ continue;
37
+ }
38
+ let agentStore;
39
+ try {
40
+ agentStore = JSON.parse(raw);
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ let merged = false;
46
+ for (const [profileId, profile] of Object.entries(agentStore.profiles ?? {})) {
47
+ if (profile.type !== "oauth")
48
+ continue;
49
+ const mainProfile = mainStore.profiles[profileId];
50
+ const mainExpiry = mainProfile?.type === "oauth" ? (mainProfile.expires ?? 0) : 0;
51
+ const agentExpiry = profile.expires ?? 0;
52
+ if (agentExpiry > mainExpiry) {
53
+ mainStore.profiles[profileId] = profile;
54
+ merged = true;
55
+ }
56
+ }
57
+ if (merged) {
58
+ changes.push(`Consolidated fresher OAuth tokens from agent "${dir}".`);
59
+ }
60
+ try {
61
+ fs.renameSync(agentAuthPath, `${agentAuthPath}.bak`);
62
+ changes.push(`Renamed ${dir}/agent/${AUTH_PROFILE_FILENAME} to .bak.`);
63
+ }
64
+ catch {
65
+ // Best-effort rename — file may be locked or read-only.
66
+ }
67
+ }
68
+ if (changes.length > 0) {
69
+ saveAuthProfileStore(mainStore);
70
+ }
71
+ return changes;
72
+ }
@@ -59,30 +59,6 @@ async function refreshOAuthTokenWithLock(params) {
59
59
  type: "oauth",
60
60
  };
61
61
  saveAuthProfileStore(store, params.agentDir);
62
- // Propagate refreshed credentials to the main store so auth.status and other agents
63
- // see the fresh token. Without this, the main store retains a stale refresh token
64
- // that Anthropic has already rotated, causing auth.status to permanently report
65
- // "Connection expired" even though the agent is working fine.
66
- const mainAuthPath = resolveAuthStorePath();
67
- if (authPath !== mainAuthPath) {
68
- try {
69
- const mainStore = ensureAuthProfileStore();
70
- const mainCred = mainStore.profiles[params.profileId];
71
- const mainExpiry = mainCred?.type === "oauth" ? mainCred.expires : 0;
72
- const freshExpiry = result.newCredentials.expires ?? 0;
73
- if (freshExpiry > mainExpiry) {
74
- mainStore.profiles[params.profileId] = {
75
- ...(mainCred ?? cred),
76
- ...result.newCredentials,
77
- type: "oauth",
78
- };
79
- saveAuthProfileStore(mainStore);
80
- }
81
- }
82
- catch {
83
- // Best-effort — don't fail the agent's own refresh if main store update fails
84
- }
85
- }
86
62
  // Sync refreshed credentials back to Claude Code CLI if this is the claude-cli profile
87
63
  // This ensures Claude Code continues to work after Taskmaster refreshes the token
88
64
  if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") {
@@ -4,12 +4,12 @@ import { saveJsonFile } from "../../infra/json-file.js";
4
4
  import { resolveUserPath } from "../../utils.js";
5
5
  import { resolveTaskmasterAgentDir } from "../agent-paths.js";
6
6
  import { AUTH_PROFILE_FILENAME, AUTH_STORE_VERSION, LEGACY_AUTH_FILENAME } from "./constants.js";
7
- export function resolveAuthStorePath(agentDir) {
8
- const resolved = resolveUserPath(agentDir ?? resolveTaskmasterAgentDir());
7
+ export function resolveAuthStorePath(_agentDir) {
8
+ const resolved = resolveUserPath(resolveTaskmasterAgentDir());
9
9
  return path.join(resolved, AUTH_PROFILE_FILENAME);
10
10
  }
11
- export function resolveLegacyAuthStorePath(agentDir) {
12
- const resolved = resolveUserPath(agentDir ?? resolveTaskmasterAgentDir());
11
+ export function resolveLegacyAuthStorePath(_agentDir) {
12
+ const resolved = resolveUserPath(resolveTaskmasterAgentDir());
13
13
  return path.join(resolved, LEGACY_AUTH_FILENAME);
14
14
  }
15
15
  export function resolveAuthStorePathForDisplay(agentDir) {
@@ -2,8 +2,8 @@ import fs from "node:fs";
2
2
  import lockfile from "proper-lockfile";
3
3
  import { resolveOAuthPath } from "../../config/paths.js";
4
4
  import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
5
- import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, CODEX_CLI_PROFILE_ID, log, } from "./constants.js";
6
- import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js";
5
+ import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
6
+ import { syncExternalCliCredentials } from "./external-cli-sync.js";
7
7
  import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
8
8
  function _syncAuthProfileStore(target, source) {
9
9
  target.version = source.version;
@@ -104,70 +104,6 @@ function coerceAuthStore(raw) {
104
104
  : undefined,
105
105
  };
106
106
  }
107
- function mergeRecord(base, override) {
108
- if (!base && !override)
109
- return undefined;
110
- if (!base)
111
- return { ...override };
112
- if (!override)
113
- return { ...base };
114
- return { ...base, ...override };
115
- }
116
- /**
117
- * Get the expiry timestamp from a credential if it has one.
118
- * Returns 0 for credentials without expiry (API keys).
119
- */
120
- function getCredentialExpiry(cred) {
121
- if (cred.type === "oauth" || cred.type === "token") {
122
- return cred.expires ?? 0;
123
- }
124
- return 0;
125
- }
126
- /**
127
- * Merge auth profiles, preferring the FRESHER credential when both have the same profile ID.
128
- * This ensures OAuth tokens propagate from the shared location to all agents.
129
- */
130
- function mergeAuthProfileStores(base, override) {
131
- if (Object.keys(override.profiles).length === 0 &&
132
- !override.order &&
133
- !override.lastGood &&
134
- !override.usageStats) {
135
- return base;
136
- }
137
- // Merge profiles, preferring the fresher credential for OAuth/token types.
138
- // For api_key profiles, prefer base (main store) — centralized API key
139
- // management writes there, and agent stores only have inherited copies.
140
- const mergedProfiles = { ...base.profiles };
141
- for (const [profileId, overrideCred] of Object.entries(override.profiles)) {
142
- const baseCred = base.profiles[profileId];
143
- if (!baseCred) {
144
- // No conflict — use override
145
- mergedProfiles[profileId] = overrideCred;
146
- }
147
- else if (baseCred.type === "api_key" && overrideCred.type === "api_key") {
148
- // API keys: base (main store) is authoritative — applyApiKeys() writes
149
- // the centralized key there. Agent stores only have stale inherited
150
- // copies. Always prefer base so key updates propagate immediately.
151
- // (keep base — already in mergedProfiles)
152
- }
153
- else {
154
- // OAuth/token: prefer the one with later expiry (fresher token)
155
- const baseExpiry = getCredentialExpiry(baseCred);
156
- const overrideExpiry = getCredentialExpiry(overrideCred);
157
- if (overrideExpiry >= baseExpiry) {
158
- mergedProfiles[profileId] = overrideCred;
159
- }
160
- // else keep base (it has a later expiry = fresher token)
161
- }
162
- }
163
- return {
164
- version: Math.max(base.version, override.version ?? base.version),
165
- profiles: mergedProfiles,
166
- order: mergeRecord(base.order, override.order),
167
- lastGood: mergeRecord(base.lastGood, override.lastGood),
168
- usageStats: mergeRecord(base.usageStats, override.usageStats),
169
- };
170
- }
171
107
  function mergeOAuthFileIntoStore(store) {
172
108
  const oauthPath = resolveOAuthPath();
173
109
  const oauthRaw = loadJsonFile(oauthPath);
@@ -249,8 +185,8 @@ export function loadAuthProfileStore() {
249
185
  syncExternalCliCredentials(store);
250
186
  return store;
251
187
  }
252
- function loadAuthProfileStoreForAgent(agentDir, options) {
253
- const authPath = resolveAuthStorePath(agentDir);
188
+ function loadAuthProfileStoreForAgent(_agentDir, options) {
189
+ const authPath = resolveAuthStorePath();
254
190
  const raw = loadJsonFile(authPath);
255
191
  const asStore = coerceAuthStore(raw);
256
192
  if (asStore) {
@@ -261,19 +197,7 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
261
197
  }
262
198
  return asStore;
263
199
  }
264
- // Fallback: inherit auth-profiles from main agent if subagent has none
265
- if (agentDir) {
266
- const mainAuthPath = resolveAuthStorePath(); // without agentDir = main
267
- const mainRaw = loadJsonFile(mainAuthPath);
268
- const mainStore = coerceAuthStore(mainRaw);
269
- if (mainStore && Object.keys(mainStore.profiles).length > 0) {
270
- // Clone main store to subagent directory for auth inheritance
271
- saveJsonFile(authPath, mainStore);
272
- log.info("inherited auth-profiles from main agent", { agentDir });
273
- return mainStore;
274
- }
275
- }
276
- const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
200
+ const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
277
201
  const legacy = coerceLegacyStore(legacyRaw);
278
202
  const store = {
279
203
  version: AUTH_STORE_VERSION,
@@ -324,7 +248,7 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
324
248
  // overwriting fresh OAuth creds with stale tokens (fixes #363). Delete only
325
249
  // after we've successfully written auth-profiles.json.
326
250
  if (shouldWrite && legacy !== null) {
327
- const legacyPath = resolveLegacyAuthStorePath(agentDir);
251
+ const legacyPath = resolveLegacyAuthStorePath();
328
252
  try {
329
253
  fs.unlinkSync(legacyPath);
330
254
  }
@@ -339,24 +263,8 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
339
263
  }
340
264
  return store;
341
265
  }
342
- export function ensureAuthProfileStore(agentDir, options) {
343
- const store = loadAuthProfileStoreForAgent(agentDir, options);
344
- const authPath = resolveAuthStorePath(agentDir);
345
- const mainAuthPath = resolveAuthStorePath();
346
- if (!agentDir || authPath === mainAuthPath) {
347
- return store;
348
- }
349
- const mainStore = loadAuthProfileStoreForAgent(undefined, options);
350
- const merged = mergeAuthProfileStores(mainStore, store);
351
- // Keep per-agent view clean even if the main store has codex-cli.
352
- const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID];
353
- if (codexProfile?.type === "oauth") {
354
- const duplicateId = findDuplicateCodexProfile(merged, codexProfile);
355
- if (duplicateId) {
356
- delete merged.profiles[CODEX_CLI_PROFILE_ID];
357
- }
358
- }
359
- return merged;
266
+ export function ensureAuthProfileStore(_agentDir, options) {
267
+ return loadAuthProfileStoreForAgent(undefined, options);
360
268
  }
361
269
  export function saveAuthProfileStore(store, agentDir) {
362
270
  const authPath = resolveAuthStorePath(agentDir);
@@ -1,6 +1,9 @@
1
+ import { emitInfraAlertEvent } from "../infra/infra-alert-events.js";
2
+ import { createSubsystemLogger } from "../logging/subsystem.js";
1
3
  import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
2
4
  import { coerceToFailoverError, describeFailoverError, isFailoverError, isTimeoutError, } from "./failover-error.js";
3
5
  import { buildModelAliasIndex, modelKey, parseModelRef, resolveConfiguredModelRef, resolveModelRefFromString, } from "./model-selection.js";
6
+ const log = createSubsystemLogger("model-fallback");
4
7
  function isAbortError(err) {
5
8
  if (!err || typeof err !== "object")
6
9
  return false;
@@ -173,6 +176,21 @@ export async function runWithModelFallback(params) {
173
176
  status: described.status,
174
177
  code: described.code,
175
178
  });
179
+ const fallbackMsg = `[model-fallback] ${candidate.provider}/${candidate.model} failed (${described.reason ?? "unknown"}): ${described.message}` +
180
+ (i + 1 < candidates.length
181
+ ? ` — falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}`
182
+ : " — no more fallback candidates");
183
+ log.warn(fallbackMsg);
184
+ console.warn(fallbackMsg);
185
+ if (i === 0) {
186
+ emitInfraAlertEvent({
187
+ category: "model-fallback",
188
+ message: `${candidate.provider}/${candidate.model} failed (${described.reason ?? "unknown"}): ${described.message}` +
189
+ (i + 1 < candidates.length
190
+ ? `. Falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}.`
191
+ : ". No fallback available."),
192
+ });
193
+ }
176
194
  await params.onError?.({
177
195
  provider: candidate.provider,
178
196
  model: candidate.model,
@@ -219,11 +237,18 @@ export async function runWithImageModelFallback(params) {
219
237
  if (shouldRethrowAbort(err))
220
238
  throw err;
221
239
  lastError = err;
240
+ const errorMsg = err instanceof Error ? err.message : String(err);
222
241
  attempts.push({
223
242
  provider: candidate.provider,
224
243
  model: candidate.model,
225
- error: err instanceof Error ? err.message : String(err),
244
+ error: errorMsg,
226
245
  });
246
+ const imgFallbackMsg = `[model-fallback] image model ${candidate.provider}/${candidate.model} failed: ${errorMsg}` +
247
+ (i + 1 < candidates.length
248
+ ? ` — falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}`
249
+ : " — no more fallback candidates");
250
+ log.warn(imgFallbackMsg);
251
+ console.warn(imgFallbackMsg);
227
252
  await params.onError?.({
228
253
  provider: candidate.provider,
229
254
  model: candidate.model,
@@ -143,6 +143,14 @@ export function buildEmbeddedRunPayloads(params) {
143
143
  });
144
144
  }
145
145
  }
146
+ // Last-resort safeguard: if the run errored and produced no user-facing content,
147
+ // inject a minimal fallback so the user isn't left with silence.
148
+ if (replyItems.length === 0 && lastAssistantErrored) {
149
+ replyItems.push({
150
+ text: "Sorry, I wasn't able to process that. Could you try again?",
151
+ isError: true,
152
+ });
153
+ }
146
154
  const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice);
147
155
  return replyItems
148
156
  .map((item) => ({
@@ -28,14 +28,15 @@ function extractToolResultId(msg) {
28
28
  return null;
29
29
  }
30
30
  function makeMissingToolResult(params) {
31
+ const name = params.toolName ?? "unknown";
31
32
  return {
32
33
  role: "toolResult",
33
34
  toolCallId: params.toolCallId,
34
- toolName: params.toolName ?? "unknown",
35
+ toolName: name,
35
36
  content: [
36
37
  {
37
38
  type: "text",
38
- text: "[taskmaster] missing tool result in session history; inserted synthetic error result for transcript repair.",
39
+ text: `[taskmaster] Tool call "${name}" did not produce a result. The tool name may be misspelled or the call was dropped. Check the exact tool name and retry.`,
39
40
  },
40
41
  ],
41
42
  isError: true,
@@ -27,6 +27,7 @@ function buildMemorySection(params) {
27
27
  return [
28
28
  "## Memory Recall",
29
29
  "Memory is your knowledge base — customer profiles, business data, preferences, prior interactions, lessons, and instructions all live there. Proactively use memory_search whenever a topic might have stored context: who a person is, what was discussed before, business rules, pricing, product details, or anything that could have been recorded. Do not rely on conversation history alone — it only covers the current session. Use memory_get to pull specific lines when you know the file. If a search returns no results, say you checked.",
30
+ "When skill files or workspace docs reference paths under `memory/` (e.g. `memory/public/data.md`), always access them with memory_get or memory_search — strip the leading `memory/` prefix to get the relative path (e.g. `public/data.md`). Never use read or skill_read for memory content.",
30
31
  "",
31
32
  ];
32
33
  }
@@ -37,6 +37,7 @@ import { createBrandSettingsTool } from "./tools/brand-settings-tool.js";
37
37
  import { createChannelSettingsTool } from "./tools/channel-settings-tool.js";
38
38
  import { createImageGenerateTool } from "./tools/image-generate-tool.js";
39
39
  import { createLogsReadTool } from "./tools/logs-read-tool.js";
40
+ import { createOpeningHoursTool } from "./tools/opening-hours-tool.js";
40
41
  import { createPublicChatSettingsTool } from "./tools/public-chat-settings-tool.js";
41
42
  import { createSkillManageTool } from "./tools/skill-manage-tool.js";
42
43
  import { createSoftwareUpdateTool } from "./tools/software-update-tool.js";
@@ -165,6 +166,7 @@ export function createTaskmasterTools(options) {
165
166
  createUsageReportTool(),
166
167
  createChannelSettingsTool(),
167
168
  createBrandSettingsTool(),
169
+ createOpeningHoursTool(),
168
170
  createPublicChatSettingsTool(),
169
171
  createSkillManageTool(),
170
172
  createLogsReadTool(),
@@ -52,6 +52,7 @@ export const TOOL_GROUPS = {
52
52
  "usage_report",
53
53
  "channel_settings",
54
54
  "brand_settings",
55
+ "opening_hours",
55
56
  "public_chat_settings",
56
57
  "skill_manage",
57
58
  "logs_read",
@@ -94,6 +95,7 @@ export const TOOL_GROUPS = {
94
95
  "usage_report",
95
96
  "channel_settings",
96
97
  "brand_settings",
98
+ "opening_hours",
97
99
  "public_chat_settings",
98
100
  "skill_manage",
99
101
  "logs_read",
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Agent tool for managing business opening hours.
3
+ *
4
+ * Wraps `config.get`/`config.patch` for reading and updating opening hours
5
+ * configuration, and `isWithinOpeningHours` for checking current status.
6
+ * Controls when the public agent responds to customer messages — outside
7
+ * opening hours, customer messages are received but the agent does not reply.
8
+ */
9
+ import { Type } from "@sinclair/typebox";
10
+ import { isPublicAgentActive } from "../../business/opening-hours.js";
11
+ import { loadConfig } from "../../config/config.js";
12
+ import { stringEnum } from "../schema/typebox.js";
13
+ import { jsonResult, readStringParam } from "./common.js";
14
+ import { callGatewayTool } from "./gateway.js";
15
+ const ACTIONS = ["get", "set", "check"];
16
+ const OpeningHoursSchema = Type.Object({
17
+ action: stringEnum(ACTIONS, {
18
+ description: "get: retrieve current opening hours config. " +
19
+ "set: update opening hours (partial update supported). " +
20
+ "check: check if the business is currently open or closed.",
21
+ }),
22
+ enabled: Type.Optional(Type.Boolean({ description: "Enable/disable opening hours schedule." })),
23
+ publicAgentEnabled: Type.Optional(Type.Boolean({ description: "Enable/disable the public agent responding." })),
24
+ schedule: Type.Optional(Type.Object({
25
+ monday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
26
+ tuesday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
27
+ wednesday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
28
+ thursday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
29
+ friday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
30
+ saturday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
31
+ sunday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
32
+ }, { description: "Per-weekday time windows. null = closed that day." })),
33
+ closedDates: Type.Optional(Type.Array(Type.String(), { description: "ISO dates (YYYY-MM-DD) to mark as closed." })),
34
+ });
35
+ export function createOpeningHoursTool() {
36
+ return {
37
+ label: "Opening Hours",
38
+ name: "opening_hours",
39
+ description: "Manage business opening hours. Controls when the public agent responds to customer messages. " +
40
+ "Outside opening hours, customer messages are received but the agent does not reply.",
41
+ parameters: OpeningHoursSchema,
42
+ execute: async (_toolCallId, args) => {
43
+ const params = args;
44
+ const action = readStringParam(params, "action", { required: true });
45
+ const gatewayOpts = {};
46
+ if (action === "check") {
47
+ const cfg = loadConfig();
48
+ const result = isPublicAgentActive(cfg.business);
49
+ return jsonResult({
50
+ currentlyOpen: result.open,
51
+ reason: result.reason,
52
+ publicAgentEnabled: cfg.business?.publicAgentEnabled !== false,
53
+ config: cfg.business?.openingHours ?? null,
54
+ });
55
+ }
56
+ if (action === "get") {
57
+ const cfg = loadConfig();
58
+ return jsonResult({
59
+ publicAgentEnabled: cfg.business?.publicAgentEnabled !== false,
60
+ openingHours: cfg.business?.openingHours ?? null,
61
+ });
62
+ }
63
+ if (action === "set") {
64
+ // Build a partial business patch.
65
+ const businessPatch = {};
66
+ // Public agent toggle lives at business.publicAgentEnabled.
67
+ if (typeof params.publicAgentEnabled === "boolean") {
68
+ businessPatch.publicAgentEnabled = params.publicAgentEnabled;
69
+ }
70
+ // Opening hours fields live at business.openingHours.
71
+ const ohPatch = {};
72
+ if (typeof params.enabled === "boolean")
73
+ ohPatch.enabled = params.enabled;
74
+ if (params.schedule)
75
+ ohPatch.schedule = params.schedule;
76
+ if (Array.isArray(params.closedDates))
77
+ ohPatch.closedDates = params.closedDates;
78
+ if (Object.keys(ohPatch).length > 0)
79
+ businessPatch.openingHours = ohPatch;
80
+ const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
81
+ const baseHash = typeof snapshot?.hash === "string" ? snapshot.hash : undefined;
82
+ const result = await callGatewayTool("config.patch", gatewayOpts, {
83
+ raw: JSON.stringify({ business: businessPatch }),
84
+ baseHash,
85
+ note: "agent: update opening hours",
86
+ });
87
+ return jsonResult({ ok: true, result });
88
+ }
89
+ throw new Error(`Unknown action: ${action}`);
90
+ },
91
+ };
92
+ }
@@ -105,7 +105,7 @@ function isRedirectStatus(status) {
105
105
  return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
106
106
  }
107
107
  async function fetchWithRedirects(params) {
108
- const signal = withTimeout(undefined, params.timeoutSeconds * 1000);
108
+ const signal = withTimeout(params.signal, params.timeoutSeconds * 1000);
109
109
  const visited = new Set();
110
110
  let currentUrl = params.url;
111
111
  let redirectCount = 0;
@@ -184,7 +184,7 @@ export async function fetchFirecrawlContent(params) {
184
184
  "Content-Type": "application/json",
185
185
  },
186
186
  body: JSON.stringify(body),
187
- signal: withTimeout(undefined, params.timeoutSeconds * 1000),
187
+ signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
188
188
  });
189
189
  const payload = (await res.json());
190
190
  if (!res.ok || payload?.success === false) {
@@ -230,6 +230,7 @@ async function runWebFetch(params) {
230
230
  maxRedirects: params.maxRedirects,
231
231
  timeoutSeconds: params.timeoutSeconds,
232
232
  userAgent: params.userAgent,
233
+ signal: params.signal,
233
234
  });
234
235
  res = result.response;
235
236
  finalUrl = result.finalUrl;
@@ -249,6 +250,7 @@ async function runWebFetch(params) {
249
250
  proxy: params.firecrawlProxy,
250
251
  storeInCache: params.firecrawlStoreInCache,
251
252
  timeoutSeconds: params.firecrawlTimeoutSeconds,
253
+ signal: params.signal,
252
254
  });
253
255
  const truncated = truncateText(firecrawl.text, params.maxChars);
254
256
  const payload = {
@@ -283,6 +285,7 @@ async function runWebFetch(params) {
283
285
  proxy: params.firecrawlProxy,
284
286
  storeInCache: params.firecrawlStoreInCache,
285
287
  timeoutSeconds: params.firecrawlTimeoutSeconds,
288
+ signal: params.signal,
286
289
  });
287
290
  const truncated = truncateText(firecrawl.text, params.maxChars);
288
291
  const payload = {
@@ -386,6 +389,7 @@ async function tryFirecrawlFallback(params) {
386
389
  proxy: params.firecrawlProxy,
387
390
  storeInCache: params.firecrawlStoreInCache,
388
391
  timeoutSeconds: params.firecrawlTimeoutSeconds,
392
+ signal: params.signal,
389
393
  });
390
394
  return { text: firecrawl.text, title: firecrawl.title };
391
395
  }
@@ -428,7 +432,7 @@ export function createWebFetchTool(options) {
428
432
  name: "web_fetch",
429
433
  description: "Fetch and extract readable content from a URL (HTML → markdown/text). Use for lightweight page access without browser automation.",
430
434
  parameters: WebFetchSchema,
431
- execute: async (_toolCallId, args) => {
435
+ execute: async (_toolCallId, args, signal) => {
432
436
  const params = args;
433
437
  const url = readStringParam(params, "url", { required: true });
434
438
  const extractMode = readStringParam(params, "extractMode") === "text" ? "text" : "markdown";
@@ -450,6 +454,7 @@ export function createWebFetchTool(options) {
450
454
  firecrawlProxy: "auto",
451
455
  firecrawlStoreInCache: true,
452
456
  firecrawlTimeoutSeconds,
457
+ signal,
453
458
  });
454
459
  return jsonResult(result);
455
460
  },
@@ -233,7 +233,7 @@ async function runPerplexitySearch(params) {
233
233
  },
234
234
  ],
235
235
  }),
236
- signal: withTimeout(undefined, params.timeoutSeconds * 1000),
236
+ signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
237
237
  });
238
238
  if (!res.ok) {
239
239
  const detail = await readResponseText(res);
@@ -256,7 +256,7 @@ async function runTavilySearch(params) {
256
256
  max_results: params.maxResults,
257
257
  search_depth: params.searchDepth,
258
258
  }),
259
- signal: withTimeout(undefined, params.timeoutSeconds * 1000),
259
+ signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
260
260
  });
261
261
  if (!res.ok) {
262
262
  const detail = await readResponseText(res);
@@ -283,6 +283,7 @@ async function runWebSearch(params) {
283
283
  baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
284
284
  model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
285
285
  timeoutSeconds: params.timeoutSeconds,
286
+ signal: params.signal,
286
287
  });
287
288
  const payload = {
288
289
  query: params.query,
@@ -302,6 +303,7 @@ async function runWebSearch(params) {
302
303
  maxResults: params.count,
303
304
  searchDepth: params.tavilySearchDepth ?? "basic",
304
305
  timeoutSeconds: params.timeoutSeconds,
306
+ signal: params.signal,
305
307
  });
306
308
  const mapped = results.map((entry) => ({
307
309
  title: entry.title ?? "",
@@ -346,7 +348,7 @@ async function runWebSearch(params) {
346
348
  Accept: "application/json",
347
349
  "X-Subscription-Token": params.apiKey,
348
350
  },
349
- signal: withTimeout(undefined, params.timeoutSeconds * 1000),
351
+ signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
350
352
  });
351
353
  if (!res.ok) {
352
354
  const detail = await readResponseText(res);
@@ -388,7 +390,7 @@ export function createWebSearchTool(options) {
388
390
  name: "web_search",
389
391
  description,
390
392
  parameters: WebSearchSchema,
391
- execute: async (_toolCallId, args) => {
393
+ execute: async (_toolCallId, args, signal) => {
392
394
  const perplexityAuth = provider === "perplexity" ? resolvePerplexityApiKey(perplexityConfig) : undefined;
393
395
  const apiKey = provider === "perplexity"
394
396
  ? perplexityAuth?.apiKey
@@ -434,6 +436,7 @@ export function createWebSearchTool(options) {
434
436
  perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source, perplexityAuth?.apiKey),
435
437
  perplexityModel: resolvePerplexityModel(perplexityConfig),
436
438
  tavilySearchDepth: resolveTavilySearchDepth(tavilyConfig),
439
+ signal,
437
440
  });
438
441
  return jsonResult(result);
439
442
  },