@rubytech/taskmaster 1.0.94 → 1.0.96

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 (45) hide show
  1. package/dist/agents/taskmaster-tools.js +4 -4
  2. package/dist/agents/tool-policy.js +2 -2
  3. package/dist/agents/tools/contact-lookup-tool.js +45 -0
  4. package/dist/agents/tools/contact-update-tool.js +68 -0
  5. package/dist/agents/tools/memory-tool.js +10 -3
  6. package/dist/build-info.json +3 -3
  7. package/dist/cli/provision-seed.js +2 -2
  8. package/dist/control-ui/assets/index-6WdtDXJj.css +1 -0
  9. package/dist/control-ui/assets/index-lbNnMWBM.js +3508 -0
  10. package/dist/control-ui/assets/index-lbNnMWBM.js.map +1 -0
  11. package/dist/control-ui/index.html +2 -2
  12. package/dist/gateway/chat-sanitize.js +121 -5
  13. package/dist/gateway/media-http.js +120 -0
  14. package/dist/gateway/protocol/schema/logs-chat.js +4 -0
  15. package/dist/gateway/public-chat-api.js +5 -3
  16. package/dist/gateway/server-http.js +3 -0
  17. package/dist/gateway/server-methods/chat.js +12 -5
  18. package/dist/gateway/server-methods/wifi.js +202 -0
  19. package/dist/gateway/server-methods.js +2 -0
  20. package/dist/infra/heartbeat-infra-alert.js +143 -0
  21. package/dist/infra/heartbeat-runner.js +13 -0
  22. package/dist/memory/manager.js +15 -8
  23. package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -0
  24. package/extensions/googlechat/node_modules/.bin/taskmaster +0 -0
  25. package/extensions/line/node_modules/.bin/taskmaster +0 -0
  26. package/extensions/matrix/node_modules/.bin/markdown-it +0 -0
  27. package/extensions/matrix/node_modules/.bin/taskmaster +0 -0
  28. package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -0
  29. package/extensions/memory-lancedb/node_modules/.bin/openai +0 -0
  30. package/extensions/msteams/node_modules/.bin/taskmaster +0 -0
  31. package/extensions/nostr/node_modules/.bin/taskmaster +0 -0
  32. package/extensions/nostr/node_modules/.bin/tsc +0 -0
  33. package/extensions/nostr/node_modules/.bin/tsserver +0 -0
  34. package/extensions/zalo/node_modules/.bin/taskmaster +0 -0
  35. package/extensions/zalouser/node_modules/.bin/taskmaster +0 -0
  36. package/package.json +64 -54
  37. package/scripts/install.sh +0 -0
  38. package/taskmaster-docs/USER-GUIDE.md +1 -1
  39. package/dist/control-ui/assets/index-B7exVNNa.css +0 -1
  40. package/dist/control-ui/assets/index-DfQL37PU.js +0 -3379
  41. package/dist/control-ui/assets/index-DfQL37PU.js.map +0 -1
  42. package/templates/.DS_Store +0 -0
  43. package/templates/customer/.DS_Store +0 -0
  44. package/templates/customer/agents/.DS_Store +0 -0
  45. package/templates/taskmaster/.gitignore +0 -1
@@ -0,0 +1,143 @@
1
+ import { describeFailoverError } from "../agents/failover-error.js";
2
+ import { getChannelPlugin } from "../channels/plugins/index.js";
3
+ import { createSubsystemLogger } from "../logging/subsystem.js";
4
+ import { deliverOutboundPayloads } from "./outbound/deliver.js";
5
+ const log = createSubsystemLogger("gateway/heartbeat-infra-alert");
6
+ const COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6 hours
7
+ const CONSECUTIVE_FAILURE_THRESHOLD = 3;
8
+ const MESSAGES = {
9
+ 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.",
10
+ billing: "Your AI assistant is offline because of a billing issue with the AI provider. Check your AI provider account.",
11
+ repeated: (n) => `Your AI assistant has failed to respond ${n} times in a row.`,
12
+ };
13
+ // In-memory cooldown map: category → last-sent timestamp.
14
+ // Resets on gateway restart — correct: admin may have restarted to fix the issue.
15
+ const cooldowns = new Map();
16
+ // Consecutive non-auth/billing failure counter. Resets when an auth/billing
17
+ // alert fires or when the heartbeat succeeds (caller responsibility via resetConsecutiveFailures).
18
+ let consecutiveUnknownFailures = 0;
19
+ function isCooledDown(category, nowMs) {
20
+ const lastSent = cooldowns.get(category);
21
+ if (typeof lastSent !== "number")
22
+ return false;
23
+ return nowMs - lastSent < COOLDOWN_MS;
24
+ }
25
+ function resolveAlertMessage(category, count) {
26
+ const template = MESSAGES[category];
27
+ if (typeof template === "function")
28
+ return template(count ?? 0);
29
+ return template;
30
+ }
31
+ /**
32
+ * Attempt to send an infra alert to the admin when a heartbeat fails due to
33
+ * auth, billing, or repeated unknown errors. Uses deliverOutboundPayloads
34
+ * directly — no agent involvement.
35
+ *
36
+ * Returns true if an alert was sent, false if suppressed or skipped.
37
+ * Never throws.
38
+ */
39
+ export async function maybeAlertAdmin(ctx) {
40
+ try {
41
+ const { cfg, delivery, err, deps } = ctx;
42
+ const nowMs = ctx.nowMs ?? Date.now();
43
+ // No delivery target — can't alert.
44
+ if (delivery.channel === "none" || !delivery.to)
45
+ return false;
46
+ const classified = describeFailoverError(err);
47
+ const reason = classified.reason;
48
+ // Auth or billing: send targeted alert.
49
+ if (reason === "auth" || reason === "billing") {
50
+ // Reset consecutive unknown failures — we have a specific diagnosis.
51
+ consecutiveUnknownFailures = 0;
52
+ const category = reason;
53
+ if (isCooledDown(category, nowMs)) {
54
+ log.debug("infra alert suppressed (cooldown)", { category });
55
+ return false;
56
+ }
57
+ // Check channel readiness before attempting delivery.
58
+ const plugin = getChannelPlugin(delivery.channel);
59
+ if (plugin?.heartbeat?.checkReady) {
60
+ const readiness = await plugin.heartbeat.checkReady({
61
+ cfg,
62
+ accountId: delivery.accountId,
63
+ deps,
64
+ });
65
+ if (!readiness.ok) {
66
+ log.info("infra alert skipped: channel not ready", {
67
+ category,
68
+ reason: readiness.reason,
69
+ });
70
+ return false;
71
+ }
72
+ }
73
+ const message = resolveAlertMessage(category);
74
+ await deliverOutboundPayloads({
75
+ cfg,
76
+ channel: delivery.channel,
77
+ to: delivery.to,
78
+ accountId: delivery.accountId,
79
+ payloads: [{ text: message }],
80
+ deps,
81
+ });
82
+ cooldowns.set(category, nowMs);
83
+ log.info("infra alert sent", { category, to: delivery.to });
84
+ return true;
85
+ }
86
+ // Non-auth/billing: track consecutive failures.
87
+ consecutiveUnknownFailures += 1;
88
+ if (consecutiveUnknownFailures >= CONSECUTIVE_FAILURE_THRESHOLD) {
89
+ const category = "repeated";
90
+ if (isCooledDown(category, nowMs)) {
91
+ log.debug("infra alert suppressed (cooldown)", { category });
92
+ return false;
93
+ }
94
+ const plugin = getChannelPlugin(delivery.channel);
95
+ if (plugin?.heartbeat?.checkReady) {
96
+ const readiness = await plugin.heartbeat.checkReady({
97
+ cfg,
98
+ accountId: delivery.accountId,
99
+ deps,
100
+ });
101
+ if (!readiness.ok) {
102
+ log.info("infra alert skipped: channel not ready", {
103
+ category,
104
+ reason: readiness.reason,
105
+ });
106
+ return false;
107
+ }
108
+ }
109
+ const message = resolveAlertMessage(category, consecutiveUnknownFailures);
110
+ await deliverOutboundPayloads({
111
+ cfg,
112
+ channel: delivery.channel,
113
+ to: delivery.to,
114
+ accountId: delivery.accountId,
115
+ payloads: [{ text: message }],
116
+ deps,
117
+ });
118
+ cooldowns.set(category, nowMs);
119
+ log.info("infra alert sent", {
120
+ category,
121
+ consecutiveFailures: consecutiveUnknownFailures,
122
+ to: delivery.to,
123
+ });
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+ catch (alertErr) {
129
+ log.error("infra alert delivery failed", {
130
+ error: alertErr instanceof Error ? alertErr.message : String(alertErr),
131
+ });
132
+ return false;
133
+ }
134
+ }
135
+ /** Reset consecutive unknown failure counter. Call when heartbeat succeeds. */
136
+ export function resetConsecutiveFailures() {
137
+ consecutiveUnknownFailures = 0;
138
+ }
139
+ /** Reset all cooldowns and counters. Exposed for testing. */
140
+ export function resetAlertCooldowns() {
141
+ cooldowns.clear();
142
+ consecutiveUnknownFailures = 0;
143
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveAgentConfig, resolveAgentWorkspaceDir, resolveDefaultAgentId, } from "../agents/agent-scope.js";
4
+ import { describeFailoverError } from "../agents/failover-error.js";
4
5
  import { resolveUserTimezone } from "../agents/date-time.js";
5
6
  import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
6
7
  import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
@@ -19,6 +20,7 @@ import { defaultRuntime } from "../runtime.js";
19
20
  import { resolveAgentBoundAccountId } from "../routing/bindings.js";
20
21
  import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
21
22
  import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
23
+ import { maybeAlertAdmin, resetConsecutiveFailures } from "./heartbeat-infra-alert.js";
22
24
  import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
23
25
  import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js";
24
26
  import { deliverOutboundPayloads } from "./outbound/deliver.js";
@@ -418,6 +420,7 @@ export async function runHeartbeatOnce(opts) {
418
420
  };
419
421
  try {
420
422
  const replyResult = await getReplyFromConfig(ctx, { isHeartbeat: true }, cfg);
423
+ resetConsecutiveFailures();
421
424
  const replyPayload = resolveHeartbeatReplyPayload(replyResult);
422
425
  const includeReasoning = heartbeat?.includeReasoning === true;
423
426
  const reasoningPayloads = includeReasoning
@@ -597,12 +600,22 @@ export async function runHeartbeatOnce(opts) {
597
600
  }
598
601
  catch (err) {
599
602
  const reason = formatErrorMessage(err);
603
+ const classified = describeFailoverError(err);
604
+ let infraAlertSent = false;
605
+ try {
606
+ infraAlertSent = await maybeAlertAdmin({ cfg, delivery, err, deps: opts.deps });
607
+ }
608
+ catch {
609
+ // Never let alert delivery crash the heartbeat runner.
610
+ }
600
611
  emitHeartbeatEvent({
601
612
  status: "failed",
602
613
  reason,
603
614
  durationMs: Date.now() - startedAt,
604
615
  channel: delivery.channel !== "none" ? delivery.channel : undefined,
605
616
  indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : undefined,
617
+ failureReason: classified.reason,
618
+ infraAlertSent,
606
619
  });
607
620
  log.error(`heartbeat failed: ${reason}`, { error: reason });
608
621
  return { status: "failed", reason };
@@ -588,19 +588,26 @@ export class MemoryIndexManager {
588
588
  return { path: relPath, bytesWritten: Buffer.byteLength(params.content, "utf-8") };
589
589
  }
590
590
  /**
591
- * Save media (binary) to a user's memory folder with session-scoped access control.
592
- * Copies from a source path (e.g., inbound media) to the user's media folder.
591
+ * Save media (binary) to a memory folder with session-scoped access control.
592
+ * Copies from a source path (e.g., inbound media) to the destination folder.
593
+ * When destFolder is omitted, defaults to memory/users/{peer}/media/.
593
594
  * Returns the destination path relative to workspace.
594
595
  */
595
596
  async writeMedia(params) {
596
- // Resolve session context to get user folder
597
597
  const sessionCtx = params.sessionContext ?? parseSessionContext(params.sessionKey);
598
- const peer = sessionCtx.peer;
599
- if (!peer) {
600
- throw new Error("session context required (peer not found)");
598
+ let relPath;
599
+ if (params.destFolder) {
600
+ // Explicit folder use as-is (scope checking enforces access)
601
+ relPath = `${params.destFolder}/${params.destFilename}`;
602
+ }
603
+ else {
604
+ // Default: memory/users/{peer}/media/{filename}
605
+ const peer = sessionCtx.peer;
606
+ if (!peer) {
607
+ throw new Error("session context required (peer not found)");
608
+ }
609
+ relPath = `memory/users/${peer}/media/${params.destFilename}`;
601
610
  }
602
- // Build destination path: memory/users/{peer}/media/{filename}
603
- const relPath = `memory/users/${peer}/media/${params.destFilename}`;
604
611
  // Apply scope filtering before writing (uses "write" scope)
605
612
  const scope = this.settings.scope;
606
613
  if (scope) {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.94",
3
+ "version": "1.0.96",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -77,12 +77,66 @@
77
77
  "dist/filler/**",
78
78
  "dist/license/**"
79
79
  ],
80
+ "scripts": {
81
+ "dev": "node scripts/run-node.mjs",
82
+ "postinstall": "node scripts/postinstall.js",
83
+ "prepack": "pnpm build && pnpm ui:build",
84
+ "docs:list": "node scripts/docs-list.js",
85
+ "docs:bin": "node scripts/build-docs-list.mjs",
86
+ "docs:dev": "cd docs && mint dev",
87
+ "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
88
+ "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
89
+ "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
90
+ "release:check": "node --import tsx scripts/release-check.ts",
91
+ "ui:install": "node scripts/ui.js install",
92
+ "ui:dev": "node scripts/ui.js dev",
93
+ "ui:build": "node scripts/ui.js build",
94
+ "start": "node scripts/run-node.mjs",
95
+ "taskmaster": "node scripts/run-node.mjs",
96
+ "gateway:watch": "node scripts/watch-node.mjs gateway --force",
97
+ "logs": "npx tsx scripts/session-viewer.ts",
98
+ "gateway:dev": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
99
+ "gateway:dev:reset": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
100
+ "tui": "node scripts/run-node.mjs tui",
101
+ "tui:dev": "TASKMASTER_PROFILE=dev node scripts/run-node.mjs tui",
102
+ "taskmaster:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
103
+ "lint": "oxlint --type-aware src test",
104
+ "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
105
+ "format": "oxfmt --check src test",
106
+ "format:fix": "oxfmt --write src test",
107
+ "test": "node scripts/test-parallel.mjs",
108
+ "test:watch": "vitest",
109
+ "test:ui": "pnpm --dir ui test",
110
+ "test:force": "node --import tsx scripts/test-force.ts",
111
+ "test:coverage": "vitest run --coverage",
112
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
113
+ "test:live": "TASKMASTER_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
114
+ "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
115
+ "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
116
+ "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
117
+ "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
118
+ "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
119
+ "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
120
+ "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
121
+ "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
122
+ "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
123
+ "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
124
+ "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
125
+ "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
126
+ "test:install:e2e:openai": "TASKMASTER_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
127
+ "test:install:e2e:anthropic": "TASKMASTER_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
128
+ "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
129
+ "protocol:check": "pnpm protocol:gen && git diff --exit-code -- dist/protocol.schema.json",
130
+ "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
131
+ "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
132
+ },
80
133
  "keywords": [],
81
134
  "author": "",
82
135
  "license": "MIT",
83
136
  "engines": {
84
137
  "node": ">=22.12.0"
85
138
  },
139
+ "packageManager": "pnpm@10.23.0",
86
140
  "dependencies": {
87
141
  "@agentclientprotocol/sdk": "0.13.1",
88
142
  "@aws-sdk/client-bedrock": "^3.975.0",
@@ -172,6 +226,14 @@
172
226
  "vitest": "^4.0.18",
173
227
  "wireit": "^0.14.12"
174
228
  },
229
+ "pnpm": {
230
+ "minimumReleaseAge": 2880,
231
+ "overrides": {
232
+ "@sinclair/typebox": "0.34.47",
233
+ "hono": "4.11.4",
234
+ "tar": "7.5.4"
235
+ }
236
+ },
175
237
  "vitest": {
176
238
  "coverage": {
177
239
  "provider": "v8",
@@ -200,57 +262,5 @@
200
262
  "**/vendor/**",
201
263
  "dist/Taskmaster.app/**"
202
264
  ]
203
- },
204
- "scripts": {
205
- "dev": "node scripts/run-node.mjs",
206
- "postinstall": "node scripts/postinstall.js",
207
- "docs:list": "node scripts/docs-list.js",
208
- "docs:bin": "node scripts/build-docs-list.mjs",
209
- "docs:dev": "cd docs && mint dev",
210
- "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
211
- "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
212
- "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
213
- "release:check": "node --import tsx scripts/release-check.ts",
214
- "ui:install": "node scripts/ui.js install",
215
- "ui:dev": "node scripts/ui.js dev",
216
- "ui:build": "node scripts/ui.js build",
217
- "start": "node scripts/run-node.mjs",
218
- "taskmaster": "node scripts/run-node.mjs",
219
- "gateway:watch": "node scripts/watch-node.mjs gateway --force",
220
- "logs": "npx tsx scripts/session-viewer.ts",
221
- "gateway:dev": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
222
- "gateway:dev:reset": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
223
- "tui": "node scripts/run-node.mjs tui",
224
- "tui:dev": "TASKMASTER_PROFILE=dev node scripts/run-node.mjs tui",
225
- "taskmaster:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
226
- "lint": "oxlint --type-aware src test",
227
- "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
228
- "format": "oxfmt --check src test",
229
- "format:fix": "oxfmt --write src test",
230
- "test": "node scripts/test-parallel.mjs",
231
- "test:watch": "vitest",
232
- "test:ui": "pnpm --dir ui test",
233
- "test:force": "node --import tsx scripts/test-force.ts",
234
- "test:coverage": "vitest run --coverage",
235
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
236
- "test:live": "TASKMASTER_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
237
- "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
238
- "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
239
- "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
240
- "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
241
- "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
242
- "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
243
- "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
244
- "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
245
- "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
246
- "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
247
- "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
248
- "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
249
- "test:install:e2e:openai": "TASKMASTER_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
250
- "test:install:e2e:anthropic": "TASKMASTER_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
251
- "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
252
- "protocol:check": "pnpm protocol:gen && git diff --exit-code -- dist/protocol.schema.json",
253
- "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
254
- "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
255
265
  }
256
- }
266
+ }
File without changes
@@ -137,7 +137,7 @@ After setup, you'll see a navigation bar at the top of the screen linking to all
137
137
  | **Setup** | Check connection status, reconnect services, link WhatsApp, manage API keys, toggle public messages |
138
138
  | **Chat** | Talk to your admin assistant directly — the preferred way to manage your business |
139
139
  | **Admins** | Choose which phone numbers get admin access (see "Two Assistants" below) |
140
- | **Customers** | Manage verified customer records (payment status, account details) that your assistant can look up but not change |
140
+ | **Contacts** | Manage verified contact records (payment status, account details) that your assistant can look up but not change |
141
141
  | **Files** | Browse and manage your assistant's knowledge files and memory |
142
142
  | **Browser** | See what the assistant sees when it browses the web for you |
143
143
  | **Advanced** | View events (automated tasks), sessions, skills, and logs |