@rubytech/taskmaster 1.0.67 → 1.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/memory-search.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.agent-runtime.js +3 -1
- package/dist/control-ui/assets/{index-Uo_tQYx1.css → index-BCh3mx9Z.css} +1 -1
- package/dist/control-ui/assets/{index-8pJBjxcK.js → index-D5tMNwub.js} +237 -237
- package/dist/control-ui/assets/index-D5tMNwub.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/control-ui.js +31 -11
- package/dist/gateway/public-chat/session.js +16 -27
- package/dist/gateway/server-methods/chat.js +3 -6
- package/dist/gateway/server-methods/memory.js +2 -0
- package/dist/gateway/server-methods/public-chat.js +22 -4
- package/dist/memory/embeddings-gemini.js +55 -23
- package/dist/memory/embeddings.js +13 -16
- package/dist/memory/manager.js +18 -1
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +2 -2
- package/dist/control-ui/assets/index-8pJBjxcK.js.map +0 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-D5tMNwub.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BCh3mx9Z.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
|
@@ -387,11 +387,16 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
387
387
|
res.end("Method Not Allowed");
|
|
388
388
|
return true;
|
|
389
389
|
}
|
|
390
|
-
// Only /public/chat (the SPA route)
|
|
391
|
-
if (
|
|
390
|
+
// Only /public/chat/:accountId (the SPA route)
|
|
391
|
+
if (!pathname.startsWith("/public/chat/")) {
|
|
392
392
|
// /public/widget.js is handled separately
|
|
393
393
|
if (pathname === "/public/widget.js")
|
|
394
394
|
return false;
|
|
395
|
+
// Bare /public/chat without accountId → 404
|
|
396
|
+
if (pathname === "/public/chat") {
|
|
397
|
+
respondNotFound(res);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
395
400
|
respondNotFound(res);
|
|
396
401
|
return true;
|
|
397
402
|
}
|
|
@@ -400,6 +405,12 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
400
405
|
respondNotFound(res);
|
|
401
406
|
return true;
|
|
402
407
|
}
|
|
408
|
+
// Extract accountId from /public/chat/:accountId
|
|
409
|
+
const accountId = pathname.slice("/public/chat/".length).split("/")[0]?.trim();
|
|
410
|
+
if (!accountId || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(accountId)) {
|
|
411
|
+
respondNotFound(res);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
403
414
|
const root = resolveControlUiRoot();
|
|
404
415
|
if (!root) {
|
|
405
416
|
res.statusCode = 503;
|
|
@@ -412,7 +423,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
412
423
|
respondNotFound(res);
|
|
413
424
|
return true;
|
|
414
425
|
}
|
|
415
|
-
const publicAgentId = resolvePublicAgentId(config);
|
|
426
|
+
const publicAgentId = resolvePublicAgentId(config, accountId);
|
|
416
427
|
const identity = resolveAssistantIdentity({ cfg: config, agentId: publicAgentId });
|
|
417
428
|
// Only inject avatar if it resolves to an actual image URL/path (not a
|
|
418
429
|
// single-letter fallback like "D" which would render as a broken <img>).
|
|
@@ -421,7 +432,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
421
432
|
agentId: publicAgentId,
|
|
422
433
|
basePath: "",
|
|
423
434
|
});
|
|
424
|
-
const avatarValue = resolvedAvatar &&
|
|
435
|
+
const avatarValue = resolvedAvatar && /^(https?:\/\/|data:image\/|\/)/i.test(resolvedAvatar)
|
|
425
436
|
? resolvedAvatar
|
|
426
437
|
: undefined;
|
|
427
438
|
const authMode = config.publicChat.auth ?? "anonymous";
|
|
@@ -438,15 +449,23 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
438
449
|
brandIconUrl,
|
|
439
450
|
accentColor,
|
|
440
451
|
});
|
|
441
|
-
// Inject
|
|
452
|
+
// Inject <base href="/"> right after <head> so relative asset paths (./assets/...)
|
|
453
|
+
// resolve from root. The URL is /public/chat/:accountId — 3 levels deep, so without
|
|
454
|
+
// <base> the browser would look for /public/chat/assets/... which doesn't exist.
|
|
455
|
+
// The <base> tag MUST appear before any tags that use relative URLs.
|
|
456
|
+
const headOpen = injected.indexOf("<head>");
|
|
457
|
+
const baseInjected = headOpen !== -1
|
|
458
|
+
? `${injected.slice(0, headOpen + 6)}<base href="/">${injected.slice(headOpen + 6)}`
|
|
459
|
+
: injected;
|
|
460
|
+
// Inject public-chat globals before </head>
|
|
442
461
|
const publicScript = `<script>` +
|
|
443
462
|
`window.__TASKMASTER_PUBLIC_CHAT__=true;` +
|
|
444
|
-
`window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ auth: authMode, cookieTtlDays })};` +
|
|
463
|
+
`window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays })};` +
|
|
445
464
|
`</script>`;
|
|
446
|
-
const headClose =
|
|
465
|
+
const headClose = baseInjected.indexOf("</head>");
|
|
447
466
|
const withPublic = headClose !== -1
|
|
448
|
-
? `${
|
|
449
|
-
: `${publicScript}${
|
|
467
|
+
? `${baseInjected.slice(0, headClose)}${publicScript}${baseInjected.slice(headClose)}`
|
|
468
|
+
: `${publicScript}${baseInjected}`;
|
|
450
469
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
451
470
|
res.setHeader("Cache-Control", "no-cache");
|
|
452
471
|
res.end(withPublic);
|
|
@@ -455,12 +474,13 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
455
474
|
/** Widget script content — self-contained JS for embedding. */
|
|
456
475
|
const WIDGET_SCRIPT = `(function(){
|
|
457
476
|
"use strict";
|
|
458
|
-
var cfg={server:""};
|
|
477
|
+
var cfg={server:"",accountId:""};
|
|
459
478
|
var isOpen=false;
|
|
460
479
|
var btn,overlay,iframe;
|
|
461
480
|
|
|
462
481
|
function init(opts){
|
|
463
482
|
if(opts&&opts.server) cfg.server=opts.server.replace(/\\/$/,"");
|
|
483
|
+
if(opts&&opts.accountId) cfg.accountId=opts.accountId;
|
|
464
484
|
build();
|
|
465
485
|
}
|
|
466
486
|
|
|
@@ -495,7 +515,7 @@ const WIDGET_SCRIPT = `(function(){
|
|
|
495
515
|
overlay.className="tm-widget-overlay";
|
|
496
516
|
iframe=document.createElement("iframe");
|
|
497
517
|
iframe.className="tm-widget-iframe";
|
|
498
|
-
iframe.src=cfg.server+"/public/chat";
|
|
518
|
+
iframe.src=cfg.server+"/public/chat/"+encodeURIComponent(cfg.accountId);
|
|
499
519
|
overlay.appendChild(iframe);
|
|
500
520
|
document.body.appendChild(overlay);
|
|
501
521
|
}
|
|
@@ -1,39 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve public-chat session keys (anonymous and verified).
|
|
3
3
|
*
|
|
4
|
-
* The public agent is the agent handling WhatsApp DMs for
|
|
4
|
+
* The public agent is the agent handling unknown WhatsApp DMs for a given account.
|
|
5
|
+
* The account is determined by the URL path: /public/chat/:accountId.
|
|
5
6
|
* Anonymous sessions use a cookie-based identifier; verified sessions use the
|
|
6
7
|
* phone number so they share the same DM session as WhatsApp.
|
|
7
8
|
*/
|
|
8
|
-
import {
|
|
9
|
+
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
|
9
10
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
|
10
11
|
/**
|
|
11
|
-
* Find the agent that handles public-facing WhatsApp DMs.
|
|
12
|
-
*
|
|
12
|
+
* Find the agent that handles public-facing WhatsApp DMs for the given account.
|
|
13
|
+
*
|
|
14
|
+
* Uses the same routing logic as WhatsApp itself: calls resolveAgentRoute
|
|
15
|
+
* with a synthetic unknown-peer DM. This guarantees the public chat routes
|
|
16
|
+
* to the exact same agent that handles unknown WhatsApp DMs for that account.
|
|
13
17
|
*/
|
|
14
|
-
export function resolvePublicAgentId(cfg) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
18
|
+
export function resolvePublicAgentId(cfg, accountId) {
|
|
19
|
+
const route = resolveAgentRoute({
|
|
20
|
+
cfg,
|
|
21
|
+
channel: "whatsapp",
|
|
22
|
+
accountId,
|
|
23
|
+
peer: { kind: "dm", id: "__public_chat__" },
|
|
24
|
+
});
|
|
25
|
+
return normalizeAgentId(route.agentId);
|
|
37
26
|
}
|
|
38
27
|
/**
|
|
39
28
|
* Build the session key for a public-chat visitor.
|
|
@@ -11,9 +11,7 @@ import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.j
|
|
|
11
11
|
import { extractShortModelName, } from "../../auto-reply/reply/response-prefix-template.js";
|
|
12
12
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
|
13
13
|
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
|
14
|
-
import { loadConfig as loadConfigFn } from "../../config/config.js";
|
|
15
14
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
|
16
|
-
import { resolvePublicAgentId } from "../public-chat/session.js";
|
|
17
15
|
import { abortChatRunById, abortChatRunsForSessionKey, isChatStopCommandText, resolveChatRunExpiresAtMs, } from "../chat-abort.js";
|
|
18
16
|
import { ErrorCodes, errorShape, formatValidationErrors, validateChatAbortParams, validateChatHistoryParams, validateChatInjectParams, validateChatSendParams, } from "../protocol/index.js";
|
|
19
17
|
import { loadSessionEntry, readSessionMessages, resolveSessionModelRef } from "../session-utils.js";
|
|
@@ -127,10 +125,9 @@ function broadcastChatError(params) {
|
|
|
127
125
|
function validatePublicSessionAccess(role, sessionKey) {
|
|
128
126
|
if (role !== "public")
|
|
129
127
|
return null;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!sessionKey.startsWith(prefix)) {
|
|
128
|
+
// Public clients can only access agent DM sessions (issued by public.session / public.otp.verify).
|
|
129
|
+
// Format: agent:{agentId}:dm:{identifier}
|
|
130
|
+
if (!/^agent:[^:]+:dm:/.test(sessionKey)) {
|
|
134
131
|
return errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized session key");
|
|
135
132
|
}
|
|
136
133
|
return null;
|
|
@@ -10,6 +10,14 @@ import { buildPublicSessionKey, resolvePublicAgentId } from "../public-chat/sess
|
|
|
10
10
|
function isValidPhone(phone) {
|
|
11
11
|
return /^\+\d{7,15}$/.test(phone);
|
|
12
12
|
}
|
|
13
|
+
function validateAccountId(raw) {
|
|
14
|
+
if (typeof raw !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (!trimmed || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed))
|
|
18
|
+
return null;
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
13
21
|
export const publicChatHandlers = {
|
|
14
22
|
/**
|
|
15
23
|
* Request an OTP code — sends a 6-digit code to the given phone via WhatsApp.
|
|
@@ -43,12 +51,13 @@ export const publicChatHandlers = {
|
|
|
43
51
|
},
|
|
44
52
|
/**
|
|
45
53
|
* Verify an OTP code and return the session key.
|
|
46
|
-
* Params: { phone: string, code: string, name?: string }
|
|
54
|
+
* Params: { phone: string, code: string, accountId: string, name?: string }
|
|
47
55
|
*/
|
|
48
56
|
"public.otp.verify": async ({ params, respond }) => {
|
|
49
57
|
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
50
58
|
const code = typeof params.code === "string" ? params.code.trim() : "";
|
|
51
59
|
const name = typeof params.name === "string" ? params.name.trim() : undefined;
|
|
60
|
+
const accountId = validateAccountId(params.accountId);
|
|
52
61
|
if (!phone || !isValidPhone(phone)) {
|
|
53
62
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid phone number"));
|
|
54
63
|
return;
|
|
@@ -57,6 +66,10 @@ export const publicChatHandlers = {
|
|
|
57
66
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "code required"));
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
69
|
+
if (!accountId) {
|
|
70
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "accountId required"));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
60
73
|
const cfg = loadConfig();
|
|
61
74
|
if (!cfg.publicChat?.enabled) {
|
|
62
75
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
@@ -73,7 +86,7 @@ export const publicChatHandlers = {
|
|
|
73
86
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, messages[result.error] ?? "verification failed"));
|
|
74
87
|
return;
|
|
75
88
|
}
|
|
76
|
-
const agentId = resolvePublicAgentId(cfg);
|
|
89
|
+
const agentId = resolvePublicAgentId(cfg, accountId);
|
|
77
90
|
const sessionKey = buildPublicSessionKey(agentId, phone);
|
|
78
91
|
respond(true, {
|
|
79
92
|
ok: true,
|
|
@@ -85,20 +98,25 @@ export const publicChatHandlers = {
|
|
|
85
98
|
},
|
|
86
99
|
/**
|
|
87
100
|
* Resolve a session key for anonymous public chat.
|
|
88
|
-
* Params: { cookieId: string }
|
|
101
|
+
* Params: { cookieId: string, accountId: string }
|
|
89
102
|
*/
|
|
90
103
|
"public.session": async ({ params, respond }) => {
|
|
91
104
|
const cookieId = typeof params.cookieId === "string" ? params.cookieId.trim() : "";
|
|
105
|
+
const accountId = validateAccountId(params.accountId);
|
|
92
106
|
if (!cookieId || cookieId.length < 8 || cookieId.length > 128) {
|
|
93
107
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cookieId"));
|
|
94
108
|
return;
|
|
95
109
|
}
|
|
110
|
+
if (!accountId) {
|
|
111
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "accountId required"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
96
114
|
const cfg = loadConfig();
|
|
97
115
|
if (!cfg.publicChat?.enabled) {
|
|
98
116
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
99
117
|
return;
|
|
100
118
|
}
|
|
101
|
-
const agentId = resolvePublicAgentId(cfg);
|
|
119
|
+
const agentId = resolvePublicAgentId(cfg, accountId);
|
|
102
120
|
const identifier = `anon-${cookieId}`;
|
|
103
121
|
const sessionKey = buildPublicSessionKey(agentId, identifier);
|
|
104
122
|
respond(true, {
|
|
@@ -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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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:
|
|
19
|
-
label: "
|
|
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;
|
|
@@ -106,7 +104,7 @@ async function createLocalEmbeddingProvider(options) {
|
|
|
106
104
|
else {
|
|
107
105
|
const selected = selectDefaultLocalModel();
|
|
108
106
|
modelPath = selected.model;
|
|
109
|
-
log.info(`selected tier ${selected.label} (system RAM: ${(os.totalmem() /
|
|
107
|
+
log.info(`selected tier ${selected.label} (system RAM: ${(os.totalmem() / 1024 ** 3).toFixed(1)} GB)`);
|
|
110
108
|
}
|
|
111
109
|
const modelCacheDir = options.local?.modelCacheDir?.trim();
|
|
112
110
|
// Lazy-load node-llama-cpp to keep startup light unless local is enabled.
|
|
@@ -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
|
|
192
|
-
// Environment variables are ignored for auto-selection to avoid
|
|
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 (!
|
|
198
|
+
if (!configKeys[configKey]?.trim())
|
|
200
199
|
continue;
|
|
201
200
|
try {
|
|
202
201
|
const result = await createProvider(id);
|
|
@@ -214,9 +213,7 @@ export async function createEmbeddingProvider(options) {
|
|
|
214
213
|
catch (err) {
|
|
215
214
|
errors.push(formatLocalSetupError(err));
|
|
216
215
|
}
|
|
217
|
-
throw new Error(errors.length > 0
|
|
218
|
-
? errors.join("\n\n")
|
|
219
|
-
: "No embeddings provider available.");
|
|
216
|
+
throw new Error(errors.length > 0 ? errors.join("\n\n") : "No embeddings provider available.");
|
|
220
217
|
}
|
|
221
218
|
try {
|
|
222
219
|
const primary = await createProvider(requestedProvider);
|
package/dist/memory/manager.js
CHANGED
|
@@ -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,22 @@ export class MemoryIndexManager {
|
|
|
509
510
|
async sync(params) {
|
|
510
511
|
if (this.syncing)
|
|
511
512
|
return this.syncing;
|
|
512
|
-
this.
|
|
513
|
+
this.syncProgress = { completed: 0, total: 0 };
|
|
514
|
+
const outerProgress = params?.progress;
|
|
515
|
+
const wrappedParams = {
|
|
516
|
+
...params,
|
|
517
|
+
progress: (update) => {
|
|
518
|
+
this.syncProgress = {
|
|
519
|
+
completed: update.completed,
|
|
520
|
+
total: update.total,
|
|
521
|
+
label: update.label,
|
|
522
|
+
};
|
|
523
|
+
outerProgress?.(update);
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
this.syncing = this.runSync(wrappedParams).finally(() => {
|
|
513
527
|
this.syncing = null;
|
|
528
|
+
this.syncProgress = null;
|
|
514
529
|
});
|
|
515
530
|
return this.syncing;
|
|
516
531
|
}
|
|
@@ -663,6 +678,8 @@ export class MemoryIndexManager {
|
|
|
663
678
|
files: files?.c ?? 0,
|
|
664
679
|
chunks: chunks?.c ?? 0,
|
|
665
680
|
dirty: this.dirty,
|
|
681
|
+
syncing: this.syncing !== null,
|
|
682
|
+
syncProgress: this.syncProgress ?? undefined,
|
|
666
683
|
workspaceDir: this.workspaceDir,
|
|
667
684
|
dbPath: this.settings.store.path,
|
|
668
685
|
provider: this.provider.id,
|
package/package.json
CHANGED
|
@@ -514,7 +514,7 @@ You get 1,000 free search credits per month — more than enough for daily busin
|
|
|
514
514
|
4. **Copy the key** (it starts with `AIza`)
|
|
515
515
|
5. Go to your Taskmaster **Setup** page, click the **API Keys** row, find **Google**, paste the key, and click **Save**
|
|
516
516
|
|
|
517
|
-
The free tier
|
|
517
|
+
The free tier works for low-volume use (a handful of voice notes or images per day). For higher volumes, you'll need to enable billing in your Google Cloud account — see [Google AI pricing](https://ai.google.dev/pricing) for details.
|
|
518
518
|
|
|
519
519
|
---
|
|
520
520
|
|
|
@@ -533,7 +533,7 @@ All files are markdown (`.md`) — plain text with simple formatting. You can up
|
|
|
533
533
|
|
|
534
534
|
When you add or change a file, your assistant picks it up automatically — no restart needed. The status light on the Files page turns red when files have changed since the last index, so you can see at a glance whether a re-index is needed.
|
|
535
535
|
|
|
536
|
-
After a fresh install or upgrade, the embedding model downloads automatically (~
|
|
536
|
+
After a fresh install or upgrade, the embedding model downloads automatically (~330 MB). During the download, a full-screen overlay blocks all navigation with a "Downloading embedding model" message — this is a one-time download and typically takes a few minutes. Memory search is unavailable until the download completes.
|
|
537
537
|
|
|
538
538
|
When your assistant writes to **public/** or **shared/**, a shield icon appears in the navigation bar so you can review what was written (see [Data Safety Alert](#data-safety-alert) above).
|
|
539
539
|
|