Merge branch 'clawdbot:main' into main
This commit is contained in:
commit
4ff0b1e443
11
CHANGELOG.md
11
CHANGELOG.md
@ -2,7 +2,13 @@
|
||||
|
||||
Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.17 (Unreleased)
|
||||
## 2026.1.17-2 (Unreleased)
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko.
|
||||
@ -10,14 +16,17 @@ Docs: https://docs.clawd.bot
|
||||
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
||||
- CLI: surface update availability in `clawdbot status`.
|
||||
- CLI: add `clawdbot memory status --deep/--index` probes.
|
||||
- CLI: add playful update completion quips.
|
||||
|
||||
### Fixes
|
||||
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
||||
- Hooks: parse multi-line/YAML frontmatter metadata blocks (JSON5-friendly). (#1114) — thanks @sebslight.
|
||||
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
|
||||
- Windows: install gateway scheduled task as the current user; show friendly guidance instead of failing on access denied.
|
||||
- Status: show both usage windows with reset hints when usage data is available. (#1101) — thanks @rhjoh.
|
||||
- Memory: probe sqlite-vec availability in `clawdbot memory status`.
|
||||
- Memory: split embedding batches to avoid OpenAI token limits during indexing.
|
||||
- Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118) — thanks @sleontenko.
|
||||
|
||||
## 2026.1.16-2
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/copilot-proxy",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-antigravity-auth",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-gemini-cli-auth",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/matrix",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Matrix channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/msteams",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/voice-call",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot voice-call plugin",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17
|
||||
|
||||
### Changes
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/zalo",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Zalo channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/zalouser",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawdbot",
|
||||
"version": "2026.1.17",
|
||||
"version": "2026.1.17-1",
|
||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@ -167,7 +167,9 @@ describe("memory cli", () => {
|
||||
registerMemoryCli(program);
|
||||
await program.parseAsync(["memory", "status", "--index"], { from: "user" });
|
||||
|
||||
expect(sync).toHaveBeenCalledWith({ reason: "cli" });
|
||||
expect(sync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ reason: "cli", progress: expect.any(Function) }),
|
||||
);
|
||||
expect(probeEmbeddingAvailability).toHaveBeenCalled();
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { withProgress, withProgressTotals } from "./progress.js";
|
||||
import { getMemorySearchManager } from "../memory/index.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
@ -46,18 +47,44 @@ export function registerMemoryCli(program: Command) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await manager.probeVectorAvailability();
|
||||
const deep = Boolean(opts.deep || opts.index);
|
||||
const embeddingProbe = deep ? await manager.probeEmbeddingAvailability() : undefined;
|
||||
let embeddingProbe: Awaited<ReturnType<typeof manager.probeEmbeddingAvailability>> | undefined;
|
||||
let indexError: string | undefined;
|
||||
if (opts.index) {
|
||||
try {
|
||||
await manager.sync({ reason: "cli" });
|
||||
} catch (err) {
|
||||
indexError = err instanceof Error ? err.message : String(err);
|
||||
defaultRuntime.error(`Memory index failed: ${indexError}`);
|
||||
process.exitCode = 1;
|
||||
if (deep) {
|
||||
await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => {
|
||||
progress.setLabel("Probing vector…");
|
||||
await manager.probeVectorAvailability();
|
||||
progress.tick();
|
||||
progress.setLabel("Probing embeddings…");
|
||||
embeddingProbe = await manager.probeEmbeddingAvailability();
|
||||
progress.tick();
|
||||
});
|
||||
if (opts.index) {
|
||||
await withProgressTotals(
|
||||
{ label: "Indexing memory…", total: 0 },
|
||||
async (update, progress) => {
|
||||
try {
|
||||
await manager.sync({
|
||||
reason: "cli",
|
||||
progress: (syncUpdate) => {
|
||||
update({
|
||||
completed: syncUpdate.completed,
|
||||
total: syncUpdate.total,
|
||||
label: syncUpdate.label,
|
||||
});
|
||||
if (syncUpdate.label) progress.setLabel(syncUpdate.label);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
indexError = err instanceof Error ? err.message : String(err);
|
||||
defaultRuntime.error(`Memory index failed: ${indexError}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await manager.probeVectorAvailability();
|
||||
}
|
||||
const status = manager.status();
|
||||
if (opts.json) {
|
||||
@ -76,6 +103,10 @@ export function registerMemoryCli(program: Command) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (opts.index) {
|
||||
const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete.";
|
||||
defaultRuntime.log(line);
|
||||
}
|
||||
const rich = isRich();
|
||||
const heading = (text: string) => colorize(rich, theme.heading, text);
|
||||
const muted = (text: string) => colorize(rich, theme.muted, text);
|
||||
|
||||
@ -72,7 +72,7 @@ export async function maybeInstallDaemon(params: {
|
||||
}
|
||||
|
||||
if (shouldInstall) {
|
||||
let installError: unknown | null = null;
|
||||
let installError: string | null = null;
|
||||
await withProgress(
|
||||
{ label: "Gateway daemon", indeterminate: true, delayMs: 0 },
|
||||
async (progress) => {
|
||||
@ -128,13 +128,13 @@ export async function maybeInstallDaemon(params: {
|
||||
});
|
||||
progress.setLabel("Gateway daemon installed.");
|
||||
} catch (err) {
|
||||
installError = err;
|
||||
installError = err instanceof Error ? err.message : String(err);
|
||||
progress.setLabel("Gateway daemon install failed.");
|
||||
}
|
||||
},
|
||||
);
|
||||
if (installError) {
|
||||
note(`Gateway daemon install failed: ${String(installError)}`, "Gateway");
|
||||
note("Gateway daemon install failed: " + installError, "Gateway");
|
||||
if (process.platform === "win32") {
|
||||
note(
|
||||
"Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install.",
|
||||
|
||||
@ -29,12 +29,15 @@ describe("gateway ws log helpers", () => {
|
||||
test("summarizeAgentEventForWsLog extracts useful fields", () => {
|
||||
const summary = summarizeAgentEventForWsLog({
|
||||
runId: "12345678-1234-1234-1234-123456789abc",
|
||||
sessionKey: "agent:main:main",
|
||||
stream: "assistant",
|
||||
seq: 2,
|
||||
data: { text: "hello world", mediaUrls: ["a", "b"] },
|
||||
});
|
||||
expect(summary).toMatchObject({
|
||||
agent: "main",
|
||||
run: "12345678…9abc",
|
||||
session: "main",
|
||||
stream: "assistant",
|
||||
aseq: 2,
|
||||
text: "hello world",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import chalk from "chalk";
|
||||
import { isVerbose } from "../globals.js";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { createSubsystemLogger, shouldLogSubsystemToConsole } from "../logging.js";
|
||||
import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js";
|
||||
import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.js";
|
||||
@ -88,11 +89,21 @@ export function summarizeAgentEventForWsLog(payload: unknown): Record<string, un
|
||||
const runId = typeof rec.runId === "string" ? rec.runId : undefined;
|
||||
const stream = typeof rec.stream === "string" ? rec.stream : undefined;
|
||||
const seq = typeof rec.seq === "number" ? rec.seq : undefined;
|
||||
const sessionKey = typeof rec.sessionKey === "string" ? rec.sessionKey : undefined;
|
||||
const data =
|
||||
rec.data && typeof rec.data === "object" ? (rec.data as Record<string, unknown>) : undefined;
|
||||
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (runId) extra.run = shortId(runId);
|
||||
if (sessionKey) {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
if (parsed) {
|
||||
extra.agent = parsed.agentId;
|
||||
extra.session = parsed.rest;
|
||||
} else {
|
||||
extra.session = sessionKey;
|
||||
}
|
||||
}
|
||||
if (stream) extra.stream = stream;
|
||||
if (seq !== undefined) extra.aseq = seq;
|
||||
|
||||
|
||||
@ -51,8 +51,14 @@ export function resetAgentRunContextForTest() {
|
||||
export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
|
||||
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
|
||||
seqByRun.set(event.runId, nextSeq);
|
||||
const context = runContextById.get(event.runId);
|
||||
const sessionKey =
|
||||
typeof event.sessionKey === "string" && event.sessionKey.trim()
|
||||
? event.sessionKey
|
||||
: context?.sessionKey;
|
||||
const enriched: AgentEventPayload = {
|
||||
...event,
|
||||
sessionKey,
|
||||
seq: nextSeq,
|
||||
ts: Date.now(),
|
||||
};
|
||||
|
||||
@ -92,6 +92,17 @@ describe("enableConsoleCapture", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("suppresses discord EventQueue slow listener duplicates", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
const warn = vi.fn();
|
||||
console.warn = warn;
|
||||
enableConsoleCapture();
|
||||
console.warn(
|
||||
"[EventQueue] Slow listener detected: DiscordMessageListener took 12.3 seconds for event MESSAGE_CREATE",
|
||||
);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not double-prefix timestamps", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
const warn = vi.fn();
|
||||
|
||||
@ -90,7 +90,16 @@ const SUPPRESSED_CONSOLE_PREFIXES = [
|
||||
|
||||
function shouldSuppressConsoleMessage(message: string): boolean {
|
||||
if (isVerbose()) return false;
|
||||
return SUPPRESSED_CONSOLE_PREFIXES.some((prefix) => message.startsWith(prefix));
|
||||
if (SUPPRESSED_CONSOLE_PREFIXES.some((prefix) => message.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
message.startsWith("[EventQueue] Slow listener detected") &&
|
||||
message.includes("DiscordMessageListener")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isEpipeError(err: unknown): boolean {
|
||||
|
||||
@ -148,7 +148,15 @@ function formatConsoleLine(opts: {
|
||||
? color.gray
|
||||
: color.cyan;
|
||||
const displayMessage = stripRedundantSubsystemPrefixForConsole(opts.message, displaySubsystem);
|
||||
const time = opts.style === "pretty" ? color.gray(new Date().toISOString().slice(11, 19)) : "";
|
||||
const time = (() => {
|
||||
if (opts.style === "pretty") {
|
||||
return color.gray(new Date().toISOString().slice(11, 19));
|
||||
}
|
||||
if (loggingState.consoleTimestampPrefix) {
|
||||
return color.gray(new Date().toISOString());
|
||||
}
|
||||
return "";
|
||||
})();
|
||||
const prefixToken = prefixColor(prefix);
|
||||
const head = [time, prefixToken].filter(Boolean).join(" ");
|
||||
return `${head} ${levelColor(displayMessage)}`;
|
||||
|
||||
@ -46,14 +46,18 @@ async function main() {
|
||||
{ setGatewayWsLogStyle },
|
||||
{ setVerbose },
|
||||
{ defaultRuntime },
|
||||
{ enableConsoleCapture, setConsoleTimestampPrefix },
|
||||
] = await Promise.all([
|
||||
import("../config/config.js"),
|
||||
import("../gateway/server.js"),
|
||||
import("../gateway/ws-logging.js"),
|
||||
import("../globals.js"),
|
||||
import("../runtime.js"),
|
||||
import("../logging.js"),
|
||||
]);
|
||||
|
||||
enableConsoleCapture();
|
||||
setConsoleTimestampPrefix(true);
|
||||
setVerbose(hasFlag(args, "--verbose"));
|
||||
|
||||
const wsLogRaw = (hasFlag(args, "--compact") ? "compact" : argValue(args, "--ws-log")) as
|
||||
|
||||
@ -109,4 +109,44 @@ describe("memory embedding batches", () => {
|
||||
|
||||
expect(embedBatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("reports sync progress totals", async () => {
|
||||
const line = "c".repeat(120);
|
||||
const content = Array.from({ length: 20 }, () => line).join("\n");
|
||||
await fs.writeFile(path.join(workspaceDir, "memory", "2026-01-05.md"), content);
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "mock-embed",
|
||||
store: { path: indexPath },
|
||||
chunking: { tokens: 200, overlap: 0 },
|
||||
sync: { watch: false, onSessionStart: false, onSearch: false },
|
||||
query: { minScore: 0 },
|
||||
},
|
||||
},
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
manager = result.manager;
|
||||
const updates: Array<{ completed: number; total: number; label?: string }> = [];
|
||||
await manager.sync({
|
||||
force: true,
|
||||
progress: (update) => {
|
||||
updates.push(update);
|
||||
},
|
||||
});
|
||||
|
||||
expect(updates.length).toBeGreaterThan(0);
|
||||
const last = updates[updates.length - 1];
|
||||
expect(last?.total).toBeGreaterThan(0);
|
||||
expect(last?.completed).toBe(last?.total);
|
||||
});
|
||||
});
|
||||
|
||||
@ -60,12 +60,24 @@ type SessionFileEntry = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
type MemorySyncProgressUpdate = {
|
||||
completed: number;
|
||||
total: number;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type MemorySyncProgressState = {
|
||||
completed: number;
|
||||
total: number;
|
||||
report: (update: MemorySyncProgressUpdate) => void;
|
||||
};
|
||||
|
||||
const META_KEY = "memory_index_meta_v1";
|
||||
const SNIPPET_MAX_CHARS = 700;
|
||||
const VECTOR_TABLE = "chunks_vec";
|
||||
const SESSION_DIRTY_DEBOUNCE_MS = 5000;
|
||||
const EMBEDDING_BATCH_MAX_TOKENS = 8000;
|
||||
const EMBEDDING_APPROX_CHARS_PER_TOKEN = 2;
|
||||
const EMBEDDING_APPROX_CHARS_PER_TOKEN = 1;
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
@ -258,7 +270,11 @@ export class MemoryIndexManager {
|
||||
}));
|
||||
}
|
||||
|
||||
async sync(params?: { reason?: string; force?: boolean }): Promise<void> {
|
||||
async sync(params?: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
progress?: (update: MemorySyncProgressUpdate) => void;
|
||||
}): Promise<void> {
|
||||
if (this.syncing) return this.syncing;
|
||||
this.syncing = this.runSync(params).finally(() => {
|
||||
this.syncing = null;
|
||||
@ -650,21 +666,46 @@ export class MemoryIndexManager {
|
||||
return this.sessionsDirty || needsFullReindex;
|
||||
}
|
||||
|
||||
private async syncMemoryFiles(params: { needsFullReindex: boolean }) {
|
||||
private async syncMemoryFiles(params: {
|
||||
needsFullReindex: boolean;
|
||||
progress?: MemorySyncProgressState;
|
||||
}) {
|
||||
const files = await listMemoryFiles(this.workspaceDir);
|
||||
const fileEntries = await Promise.all(
|
||||
files.map(async (file) => buildFileEntry(file, this.workspaceDir)),
|
||||
);
|
||||
const activePaths = new Set(fileEntries.map((entry) => entry.path));
|
||||
if (params.progress) {
|
||||
params.progress.total += fileEntries.length;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
label: "Indexing memory files…",
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of fileEntries) {
|
||||
const record = this.db
|
||||
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
||||
.get(entry.path, "memory") as { hash: string } | undefined;
|
||||
if (!params.needsFullReindex && record?.hash === entry.hash) {
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await this.indexFile(entry, { source: "memory" });
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const staleRows = this.db
|
||||
@ -677,22 +718,65 @@ export class MemoryIndexManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncSessionFiles(params: { needsFullReindex: boolean }) {
|
||||
private async syncSessionFiles(params: {
|
||||
needsFullReindex: boolean;
|
||||
progress?: MemorySyncProgressState;
|
||||
}) {
|
||||
const files = await this.listSessionFiles();
|
||||
const activePaths = new Set(files.map((file) => this.sessionPathForFile(file)));
|
||||
const indexAll = params.needsFullReindex || this.sessionsDirtyFiles.size === 0;
|
||||
if (params.progress) {
|
||||
params.progress.total += files.length;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
label: "Indexing session files…",
|
||||
});
|
||||
}
|
||||
|
||||
for (const absPath of files) {
|
||||
if (!indexAll && !this.sessionsDirtyFiles.has(absPath)) continue;
|
||||
if (!indexAll && !this.sessionsDirtyFiles.has(absPath)) {
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const entry = await this.buildSessionEntry(absPath);
|
||||
if (!entry) continue;
|
||||
if (!entry) {
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const record = this.db
|
||||
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
||||
.get(entry.path, "sessions") as { hash: string } | undefined;
|
||||
if (!params.needsFullReindex && record?.hash === entry.hash) {
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await this.indexFile(entry, { source: "sessions", content: entry.content });
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
completed: params.progress.completed,
|
||||
total: params.progress.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const staleRows = this.db
|
||||
@ -709,7 +793,14 @@ export class MemoryIndexManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async runSync(params?: { reason?: string; force?: boolean }) {
|
||||
private async runSync(params?: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
progress?: (update: MemorySyncProgressUpdate) => void;
|
||||
}) {
|
||||
const progress: MemorySyncProgressState | null = params?.progress
|
||||
? { completed: 0, total: 0, report: params.progress }
|
||||
: null;
|
||||
const vectorReady = await this.ensureVectorReady();
|
||||
const meta = this.readMeta();
|
||||
const needsFullReindex =
|
||||
@ -729,12 +820,12 @@ export class MemoryIndexManager {
|
||||
const shouldSyncSessions = this.shouldSyncSessions(params, needsFullReindex);
|
||||
|
||||
if (shouldSyncMemory) {
|
||||
await this.syncMemoryFiles({ needsFullReindex });
|
||||
await this.syncMemoryFiles({ needsFullReindex, progress: progress ?? undefined });
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
if (shouldSyncSessions) {
|
||||
await this.syncSessionFiles({ needsFullReindex });
|
||||
await this.syncSessionFiles({ needsFullReindex, progress: progress ?? undefined });
|
||||
this.sessionsDirty = false;
|
||||
this.sessionsDirtyFiles.clear();
|
||||
} else if (needsFullReindex && this.sources.has("sessions")) {
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
buildTelegramGroupFrom,
|
||||
buildTelegramGroupPeerId,
|
||||
buildTypingThreadParams,
|
||||
expandTextLinks,
|
||||
normalizeForwardedContext,
|
||||
describeReplyTarget,
|
||||
extractTelegramLocation,
|
||||
@ -271,7 +272,8 @@ export const buildTelegramMessageContext = async ({
|
||||
|
||||
const locationData = extractTelegramLocation(msg);
|
||||
const locationText = locationData ? formatLocationText(locationData) : undefined;
|
||||
const rawText = (msg.text ?? msg.caption ?? "").trim();
|
||||
const rawTextSource = msg.text ?? msg.caption ?? "";
|
||||
const rawText = expandTextLinks(rawTextSource, msg.entities ?? msg.caption_entities).trim();
|
||||
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
|
||||
if (!rawBody) rawBody = placeholder;
|
||||
if (!rawBody && allMedia.length === 0) return null;
|
||||
|
||||
55
src/telegram/bot/helpers.expand-text-links.test.ts
Normal file
55
src/telegram/bot/helpers.expand-text-links.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { expandTextLinks } from "./helpers.js";
|
||||
|
||||
describe("expandTextLinks", () => {
|
||||
it("returns text unchanged when no entities are provided", () => {
|
||||
expect(expandTextLinks("Hello world")).toBe("Hello world");
|
||||
expect(expandTextLinks("Hello world", null)).toBe("Hello world");
|
||||
expect(expandTextLinks("Hello world", [])).toBe("Hello world");
|
||||
});
|
||||
|
||||
it("returns text unchanged when there are no text_link entities", () => {
|
||||
const entities = [
|
||||
{ type: "mention", offset: 0, length: 5 },
|
||||
{ type: "bold", offset: 6, length: 5 },
|
||||
];
|
||||
expect(expandTextLinks("@user hello", entities)).toBe("@user hello");
|
||||
});
|
||||
|
||||
it("expands a single text_link entity", () => {
|
||||
const text = "Check this link for details";
|
||||
const entities = [{ type: "text_link", offset: 11, length: 4, url: "https://example.com" }];
|
||||
expect(expandTextLinks(text, entities)).toBe(
|
||||
"Check this [link](https://example.com) for details",
|
||||
);
|
||||
});
|
||||
|
||||
it("expands multiple text_link entities", () => {
|
||||
const text = "Visit Google or GitHub for more";
|
||||
const entities = [
|
||||
{ type: "text_link", offset: 6, length: 6, url: "https://google.com" },
|
||||
{ type: "text_link", offset: 16, length: 6, url: "https://github.com" },
|
||||
];
|
||||
expect(expandTextLinks(text, entities)).toBe(
|
||||
"Visit [Google](https://google.com) or [GitHub](https://github.com) for more",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles adjacent text_link entities", () => {
|
||||
const text = "AB";
|
||||
const entities = [
|
||||
{ type: "text_link", offset: 0, length: 1, url: "https://a.example" },
|
||||
{ type: "text_link", offset: 1, length: 1, url: "https://b.example" },
|
||||
];
|
||||
expect(expandTextLinks(text, entities)).toBe("[A](https://a.example)[B](https://b.example)");
|
||||
});
|
||||
|
||||
it("preserves offsets from the original string", () => {
|
||||
const text = " Hello world";
|
||||
const entities = [{ type: "text_link", offset: 1, length: 5, url: "https://example.com" }];
|
||||
expect(expandTextLinks(text, entities)).toBe(
|
||||
" [Hello](https://example.com) world",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -114,6 +114,37 @@ export function hasBotMention(msg: TelegramMessage, botUsername: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
type TelegramTextLinkEntity = {
|
||||
type: string;
|
||||
offset: number;
|
||||
length: number;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export function expandTextLinks(
|
||||
text: string,
|
||||
entities?: TelegramTextLinkEntity[] | null,
|
||||
): string {
|
||||
if (!text || !entities?.length) return text;
|
||||
|
||||
const textLinks = entities
|
||||
.filter(
|
||||
(entity): entity is TelegramTextLinkEntity & { url: string } =>
|
||||
entity.type === "text_link" && Boolean(entity.url),
|
||||
)
|
||||
.sort((a, b) => b.offset - a.offset);
|
||||
|
||||
if (textLinks.length === 0) return text;
|
||||
|
||||
let result = text;
|
||||
for (const entity of textLinks) {
|
||||
const linkText = text.slice(entity.offset, entity.offset + entity.length);
|
||||
const markdown = `[${linkText}](${entity.url})`;
|
||||
result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveTelegramReplyId(raw?: string): number | undefined {
|
||||
if (!raw) return undefined;
|
||||
const parsed = Number(raw);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user