@rubytech/taskmaster 1.0.67 → 1.0.68

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.
@@ -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-8pJBjxcK.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-Uo_tQYx1.css">
9
+ <script type="module" crossorigin src="./assets/index-Tpr1NFEw.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-BCh3mx9Z.css">
11
11
  </head>
12
12
  <body>
13
13
  <taskmaster-app></taskmaster-app>
@@ -5,35 +5,27 @@
5
5
  * Anonymous sessions use a cookie-based identifier; verified sessions use the
6
6
  * phone number so they share the same DM session as WhatsApp.
7
7
  */
8
- import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
8
+ import { listBoundAccountIds } from "../../routing/bindings.js";
9
+ import { resolveAgentRoute } from "../../routing/resolve-route.js";
9
10
  import { normalizeAgentId } from "../../routing/session-key.js";
10
11
  /**
11
12
  * Find the agent that handles public-facing WhatsApp DMs.
12
- * Priority: binding to whatsapp DM > agent named "public" > default agent.
13
+ *
14
+ * Uses the same routing logic as WhatsApp itself: calls resolveAgentRoute
15
+ * with a synthetic unknown-peer DM on the first WhatsApp account. This
16
+ * guarantees the public chat routes to the exact same agent that handles
17
+ * unknown WhatsApp DMs.
13
18
  */
14
19
  export function resolvePublicAgentId(cfg) {
15
- const bindings = cfg.bindings ?? [];
16
- // Find agent bound to whatsapp DMs (the public-facing agent)
17
- for (const binding of bindings) {
18
- if (binding.match.channel === "whatsapp" &&
19
- binding.match.peer?.kind === "dm" &&
20
- !binding.match.peer.id) {
21
- return normalizeAgentId(binding.agentId);
22
- }
23
- }
24
- // Any whatsapp binding
25
- for (const binding of bindings) {
26
- if (binding.match.channel === "whatsapp") {
27
- return normalizeAgentId(binding.agentId);
28
- }
29
- }
30
- // Agent explicitly named "public"
31
- const agents = cfg.agents?.list ?? [];
32
- const publicAgent = agents.find((a) => a.id === "public");
33
- if (publicAgent)
34
- return normalizeAgentId(publicAgent.id);
35
- // Fall back to default agent
36
- return resolveDefaultAgentId(cfg);
20
+ const accountIds = listBoundAccountIds(cfg, "whatsapp");
21
+ const accountId = accountIds[0] ?? "default";
22
+ const route = resolveAgentRoute({
23
+ cfg,
24
+ channel: "whatsapp",
25
+ accountId,
26
+ peer: { kind: "dm", id: "__public_chat__" },
27
+ });
28
+ return normalizeAgentId(route.agentId);
37
29
  }
38
30
  /**
39
31
  * Build the session key for a public-chat visitor.
@@ -20,6 +20,8 @@ export const memoryHandlers = {
20
20
  ok: true,
21
21
  agentId,
22
22
  dirty: status.dirty,
23
+ syncing: status.syncing,
24
+ syncProgress: status.syncProgress,
23
25
  files: status.files,
24
26
  chunks: status.chunks,
25
27
  sources: status.sources,
@@ -41,6 +41,14 @@ function normalizeGeminiBaseUrl(raw) {
41
41
  function buildGeminiModelPath(model) {
42
42
  return model.startsWith("models/") ? model : `models/${model}`;
43
43
  }
44
+ /** Extract retry delay from a Gemini 429 response body, defaulting to 60s. */
45
+ function parseRetryDelay(body) {
46
+ // Gemini includes "retryDelay": "52s" in the response.
47
+ const match = body.match(/"retryDelay"\s*:\s*"(\d+)s?"/);
48
+ if (match)
49
+ return Math.max(1, Number(match[1]));
50
+ return 60;
51
+ }
44
52
  export async function createGeminiEmbeddingProvider(options) {
45
53
  const client = await resolveGeminiEmbeddingClient(options);
46
54
  const baseUrl = client.baseUrl.replace(/\/$/, "");
@@ -49,20 +57,31 @@ export async function createGeminiEmbeddingProvider(options) {
49
57
  const embedQuery = async (text) => {
50
58
  if (!text.trim())
51
59
  return [];
52
- const res = await fetch(embedUrl, {
53
- method: "POST",
54
- headers: client.headers,
55
- body: JSON.stringify({
56
- content: { parts: [{ text }] },
57
- taskType: "RETRIEVAL_QUERY",
58
- }),
60
+ const body = JSON.stringify({
61
+ content: { parts: [{ text }] },
62
+ taskType: "RETRIEVAL_QUERY",
59
63
  });
60
- if (!res.ok) {
61
- const payload = await res.text();
62
- throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
64
+ const maxRetries = 3;
65
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
66
+ const res = await fetch(embedUrl, {
67
+ method: "POST",
68
+ headers: client.headers,
69
+ body,
70
+ });
71
+ if (res.status === 429 && attempt < maxRetries) {
72
+ const retryAfter = parseRetryDelay(await res.text());
73
+ log.info(`gemini rate limit hit; retrying in ${retryAfter}s`);
74
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
75
+ continue;
76
+ }
77
+ if (!res.ok) {
78
+ const payload = await res.text();
79
+ throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
80
+ }
81
+ const payload = (await res.json());
82
+ return payload.embedding?.values ?? [];
63
83
  }
64
- const payload = (await res.json());
65
- return payload.embedding?.values ?? [];
84
+ throw new Error("gemini embeddings: exhausted retries after rate limiting");
66
85
  };
67
86
  const embedBatch = async (texts) => {
68
87
  if (texts.length === 0)
@@ -72,18 +91,31 @@ export async function createGeminiEmbeddingProvider(options) {
72
91
  content: { parts: [{ text }] },
73
92
  taskType: "RETRIEVAL_DOCUMENT",
74
93
  }));
75
- const res = await fetch(batchUrl, {
76
- method: "POST",
77
- headers: client.headers,
78
- body: JSON.stringify({ requests }),
79
- });
80
- if (!res.ok) {
81
- const payload = await res.text();
82
- throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
94
+ const body = JSON.stringify({ requests });
95
+ // Retry on 429 (rate limit) — Gemini free tier caps at 100 requests/minute.
96
+ // The initial bulk index can exceed this; retrying after the cooldown lets it complete.
97
+ const maxRetries = 3;
98
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
99
+ const res = await fetch(batchUrl, {
100
+ method: "POST",
101
+ headers: client.headers,
102
+ body,
103
+ });
104
+ if (res.status === 429 && attempt < maxRetries) {
105
+ const retryAfter = parseRetryDelay(await res.text());
106
+ log.info(`gemini rate limit hit; retrying in ${retryAfter}s`);
107
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
108
+ continue;
109
+ }
110
+ if (!res.ok) {
111
+ const payload = await res.text();
112
+ throw new Error(`gemini embeddings failed: ${res.status} ${payload}`);
113
+ }
114
+ const payload = (await res.json());
115
+ const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : [];
116
+ return texts.map((_, index) => embeddings[index]?.values ?? []);
83
117
  }
84
- const payload = (await res.json());
85
- const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : [];
86
- return texts.map((_, index) => embeddings[index]?.values ?? []);
118
+ throw new Error("gemini embeddings: exhausted retries after rate limiting");
87
119
  };
88
120
  return {
89
121
  provider: {
@@ -2,21 +2,19 @@ import fsSync from "node:fs";
2
2
  import os from "node:os";
3
3
  import { createSubsystemLogger } from "../logging/subsystem.js";
4
4
  import { resolveUserPath } from "../utils.js";
5
- import { getCustomProviderApiKey } from "../agents/model-auth.js";
6
5
  import { createGeminiEmbeddingProvider } from "./embeddings-gemini.js";
7
6
  import { createOpenAiEmbeddingProvider } from "./embeddings-openai.js";
8
7
  import { importNodeLlamaCpp } from "./node-llama.js";
9
8
  /**
10
- * Default local embedding model. The 0.6B model is small enough to run on
11
- * any target hardware (Pi 4GB, Pi 8GB, Mac) without GPU and with minimal
12
- * RAM overhead (~1-2 GB runtime). Larger models (4B, 8B) can be configured
13
- * explicitly via `local.modelPath` for users who have tested them on their
14
- * specific hardware node-llama-cpp's actual memory footprint varies
15
- * enormously across GPU vendors and Metal/Vulkan backends.
9
+ * Default local embedding model. embeddinggemma (329 MB) is proven stable on
10
+ * Intel x64 Macs and ARM Pis via node-llama-cpp's CPU backend. Larger models
11
+ * (Qwen3-Embedding 0.6B/4B/8B) can be configured explicitly via
12
+ * `local.modelPath` but are untested across hardware Qwen3 models caused
13
+ * runaway memory consumption (40-76 GB) on Intel x64 Mac + AMD GPU.
16
14
  */
17
15
  const DEFAULT_LOCAL_MODEL = {
18
- model: "hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf",
19
- label: "Qwen3-Embedding-0.6B",
16
+ model: "hf:ggml-org/embeddinggemma-300M-Q8_0-GGUF/embeddinggemma-300M-Q8_0.gguf",
17
+ label: "embeddinggemma-300M",
20
18
  };
21
19
  function selectDefaultLocalModel() {
22
20
  return DEFAULT_LOCAL_MODEL;
@@ -188,15 +186,16 @@ export async function createEmbeddingProvider(options) {
188
186
  }
189
187
  // 2. Try remote providers — preferred when API keys are available
190
188
  // (faster, no RAM overhead, no model download).
191
- // Only consider providers whose key is in the config (apiKeys section).
192
- // Environment variables are ignored for auto-selection to avoid using
189
+ // Only consider providers whose key is in config.apiKeys.
190
+ // Environment variables are ignored for auto-selection to avoid
193
191
  // stale dev keys that would fail at runtime.
192
+ const configKeys = options.config.apiKeys ?? {};
194
193
  const remoteProviders = [
195
194
  { id: "openai", configKey: "openai" },
196
195
  { id: "gemini", configKey: "google" },
197
196
  ];
198
197
  for (const { id, configKey } of remoteProviders) {
199
- if (!getCustomProviderApiKey(options.config, configKey))
198
+ if (!configKeys[configKey]?.trim())
200
199
  continue;
201
200
  try {
202
201
  const result = await createProvider(id);
@@ -282,6 +282,7 @@ export class MemoryIndexManager {
282
282
  sessionDeltas = new Map();
283
283
  sessionWarm = new Set();
284
284
  syncing = null;
285
+ syncProgress = null;
285
286
  /**
286
287
  * Ensure standard memory directory structure exists.
287
288
  * Creates: memory/public, memory/shared, memory/admin, memory/users
@@ -509,8 +510,18 @@ export class MemoryIndexManager {
509
510
  async sync(params) {
510
511
  if (this.syncing)
511
512
  return this.syncing;
512
- this.syncing = this.runSync(params).finally(() => {
513
+ this.syncProgress = { completed: 0, total: 0 };
514
+ const outerProgress = params?.progress;
515
+ const wrappedParams = {
516
+ ...params,
517
+ progress: (update) => {
518
+ this.syncProgress = { completed: update.completed, total: update.total };
519
+ outerProgress?.(update);
520
+ },
521
+ };
522
+ this.syncing = this.runSync(wrappedParams).finally(() => {
513
523
  this.syncing = null;
524
+ this.syncProgress = null;
514
525
  });
515
526
  return this.syncing;
516
527
  }
@@ -663,6 +674,8 @@ export class MemoryIndexManager {
663
674
  files: files?.c ?? 0,
664
675
  chunks: chunks?.c ?? 0,
665
676
  dirty: this.dirty,
677
+ syncing: this.syncing !== null,
678
+ syncProgress: this.syncProgress ?? undefined,
666
679
  workspaceDir: this.workspaceDir,
667
680
  dbPath: this.settings.store.path,
668
681
  provider: this.provider.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"