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
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.17 (Unreleased)
|
## 2026.1.17-2 (Unreleased)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
## 2026.1.17-1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko.
|
- 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.
|
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
||||||
- CLI: surface update availability in `clawdbot status`.
|
- CLI: surface update availability in `clawdbot status`.
|
||||||
- CLI: add `clawdbot memory status --deep/--index` probes.
|
- CLI: add `clawdbot memory status --deep/--index` probes.
|
||||||
|
- CLI: add playful update completion quips.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
- 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.
|
- Hooks: parse multi-line/YAML frontmatter metadata blocks (JSON5-friendly). (#1114) — thanks @sebslight.
|
||||||
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
|
- 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.
|
- 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: probe sqlite-vec availability in `clawdbot memory status`.
|
||||||
- Memory: split embedding batches to avoid OpenAI token limits during indexing.
|
- 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
|
## 2026.1.16-2
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/copilot-proxy",
|
"name": "@clawdbot/copilot-proxy",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/google-antigravity-auth",
|
"name": "@clawdbot/google-antigravity-auth",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/google-gemini-cli-auth",
|
"name": "@clawdbot/google-gemini-cli-auth",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026.1.17-1
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Version alignment with core Clawdbot release numbers.
|
||||||
|
|
||||||
## 2026.1.17
|
## 2026.1.17
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/matrix",
|
"name": "@clawdbot/matrix",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Matrix channel plugin",
|
"description": "Clawdbot Matrix channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026.1.17-1
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Version alignment with core Clawdbot release numbers.
|
||||||
|
|
||||||
## 2026.1.17
|
## 2026.1.17
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/msteams",
|
"name": "@clawdbot/msteams",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026.1.17-1
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Version alignment with core Clawdbot release numbers.
|
||||||
|
|
||||||
## 2026.1.17
|
## 2026.1.17
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/voice-call",
|
"name": "@clawdbot/voice-call",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot voice-call plugin",
|
"description": "Clawdbot voice-call plugin",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026.1.17-1
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Version alignment with core Clawdbot release numbers.
|
||||||
|
|
||||||
## 2026.1.17
|
## 2026.1.17
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/zalo",
|
"name": "@clawdbot/zalo",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Zalo channel plugin",
|
"description": "Clawdbot Zalo channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/zalouser",
|
"name": "@clawdbot/zalouser",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
|
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clawdbot",
|
"name": "clawdbot",
|
||||||
"version": "2026.1.17",
|
"version": "2026.1.17-1",
|
||||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@ -167,7 +167,9 @@ describe("memory cli", () => {
|
|||||||
registerMemoryCli(program);
|
registerMemoryCli(program);
|
||||||
await program.parseAsync(["memory", "status", "--index"], { from: "user" });
|
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(probeEmbeddingAvailability).toHaveBeenCalled();
|
||||||
expect(close).toHaveBeenCalled();
|
expect(close).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|||||||
|
|
||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import { withProgress, withProgressTotals } from "./progress.js";
|
||||||
import { getMemorySearchManager } from "../memory/index.js";
|
import { getMemorySearchManager } from "../memory/index.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
@ -46,18 +47,44 @@ export function registerMemoryCli(program: Command) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await manager.probeVectorAvailability();
|
|
||||||
const deep = Boolean(opts.deep || opts.index);
|
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;
|
let indexError: string | undefined;
|
||||||
if (opts.index) {
|
if (deep) {
|
||||||
try {
|
await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => {
|
||||||
await manager.sync({ reason: "cli" });
|
progress.setLabel("Probing vector…");
|
||||||
} catch (err) {
|
await manager.probeVectorAvailability();
|
||||||
indexError = err instanceof Error ? err.message : String(err);
|
progress.tick();
|
||||||
defaultRuntime.error(`Memory index failed: ${indexError}`);
|
progress.setLabel("Probing embeddings…");
|
||||||
process.exitCode = 1;
|
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();
|
const status = manager.status();
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
@ -76,6 +103,10 @@ export function registerMemoryCli(program: Command) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (opts.index) {
|
||||||
|
const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete.";
|
||||||
|
defaultRuntime.log(line);
|
||||||
|
}
|
||||||
const rich = isRich();
|
const rich = isRich();
|
||||||
const heading = (text: string) => colorize(rich, theme.heading, text);
|
const heading = (text: string) => colorize(rich, theme.heading, text);
|
||||||
const muted = (text: string) => colorize(rich, theme.muted, text);
|
const muted = (text: string) => colorize(rich, theme.muted, text);
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export async function maybeInstallDaemon(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldInstall) {
|
if (shouldInstall) {
|
||||||
let installError: unknown | null = null;
|
let installError: string | null = null;
|
||||||
await withProgress(
|
await withProgress(
|
||||||
{ label: "Gateway daemon", indeterminate: true, delayMs: 0 },
|
{ label: "Gateway daemon", indeterminate: true, delayMs: 0 },
|
||||||
async (progress) => {
|
async (progress) => {
|
||||||
@ -128,13 +128,13 @@ export async function maybeInstallDaemon(params: {
|
|||||||
});
|
});
|
||||||
progress.setLabel("Gateway daemon installed.");
|
progress.setLabel("Gateway daemon installed.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
installError = err;
|
installError = err instanceof Error ? err.message : String(err);
|
||||||
progress.setLabel("Gateway daemon install failed.");
|
progress.setLabel("Gateway daemon install failed.");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (installError) {
|
if (installError) {
|
||||||
note(`Gateway daemon install failed: ${String(installError)}`, "Gateway");
|
note("Gateway daemon install failed: " + installError, "Gateway");
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
note(
|
note(
|
||||||
"Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install.",
|
"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", () => {
|
test("summarizeAgentEventForWsLog extracts useful fields", () => {
|
||||||
const summary = summarizeAgentEventForWsLog({
|
const summary = summarizeAgentEventForWsLog({
|
||||||
runId: "12345678-1234-1234-1234-123456789abc",
|
runId: "12345678-1234-1234-1234-123456789abc",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
stream: "assistant",
|
stream: "assistant",
|
||||||
seq: 2,
|
seq: 2,
|
||||||
data: { text: "hello world", mediaUrls: ["a", "b"] },
|
data: { text: "hello world", mediaUrls: ["a", "b"] },
|
||||||
});
|
});
|
||||||
expect(summary).toMatchObject({
|
expect(summary).toMatchObject({
|
||||||
|
agent: "main",
|
||||||
run: "12345678…9abc",
|
run: "12345678…9abc",
|
||||||
|
session: "main",
|
||||||
stream: "assistant",
|
stream: "assistant",
|
||||||
aseq: 2,
|
aseq: 2,
|
||||||
text: "hello world",
|
text: "hello world",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { isVerbose } from "../globals.js";
|
import { isVerbose } from "../globals.js";
|
||||||
|
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||||
import { createSubsystemLogger, shouldLogSubsystemToConsole } from "../logging.js";
|
import { createSubsystemLogger, shouldLogSubsystemToConsole } from "../logging.js";
|
||||||
import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js";
|
import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js";
|
||||||
import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.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 runId = typeof rec.runId === "string" ? rec.runId : undefined;
|
||||||
const stream = typeof rec.stream === "string" ? rec.stream : undefined;
|
const stream = typeof rec.stream === "string" ? rec.stream : undefined;
|
||||||
const seq = typeof rec.seq === "number" ? rec.seq : undefined;
|
const seq = typeof rec.seq === "number" ? rec.seq : undefined;
|
||||||
|
const sessionKey = typeof rec.sessionKey === "string" ? rec.sessionKey : undefined;
|
||||||
const data =
|
const data =
|
||||||
rec.data && typeof rec.data === "object" ? (rec.data as Record<string, unknown>) : undefined;
|
rec.data && typeof rec.data === "object" ? (rec.data as Record<string, unknown>) : undefined;
|
||||||
|
|
||||||
const extra: Record<string, unknown> = {};
|
const extra: Record<string, unknown> = {};
|
||||||
if (runId) extra.run = shortId(runId);
|
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 (stream) extra.stream = stream;
|
||||||
if (seq !== undefined) extra.aseq = seq;
|
if (seq !== undefined) extra.aseq = seq;
|
||||||
|
|
||||||
|
|||||||
@ -51,8 +51,14 @@ export function resetAgentRunContextForTest() {
|
|||||||
export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
|
export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
|
||||||
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
|
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
|
||||||
seqByRun.set(event.runId, nextSeq);
|
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 = {
|
const enriched: AgentEventPayload = {
|
||||||
...event,
|
...event,
|
||||||
|
sessionKey,
|
||||||
seq: nextSeq,
|
seq: nextSeq,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,6 +92,17 @@ describe("enableConsoleCapture", () => {
|
|||||||
vi.useRealTimers();
|
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", () => {
|
it("does not double-prefix timestamps", () => {
|
||||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||||
const warn = vi.fn();
|
const warn = vi.fn();
|
||||||
|
|||||||
@ -90,7 +90,16 @@ const SUPPRESSED_CONSOLE_PREFIXES = [
|
|||||||
|
|
||||||
function shouldSuppressConsoleMessage(message: string): boolean {
|
function shouldSuppressConsoleMessage(message: string): boolean {
|
||||||
if (isVerbose()) return false;
|
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 {
|
function isEpipeError(err: unknown): boolean {
|
||||||
|
|||||||
@ -148,7 +148,15 @@ function formatConsoleLine(opts: {
|
|||||||
? color.gray
|
? color.gray
|
||||||
: color.cyan;
|
: color.cyan;
|
||||||
const displayMessage = stripRedundantSubsystemPrefixForConsole(opts.message, displaySubsystem);
|
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 prefixToken = prefixColor(prefix);
|
||||||
const head = [time, prefixToken].filter(Boolean).join(" ");
|
const head = [time, prefixToken].filter(Boolean).join(" ");
|
||||||
return `${head} ${levelColor(displayMessage)}`;
|
return `${head} ${levelColor(displayMessage)}`;
|
||||||
|
|||||||
@ -46,14 +46,18 @@ async function main() {
|
|||||||
{ setGatewayWsLogStyle },
|
{ setGatewayWsLogStyle },
|
||||||
{ setVerbose },
|
{ setVerbose },
|
||||||
{ defaultRuntime },
|
{ defaultRuntime },
|
||||||
|
{ enableConsoleCapture, setConsoleTimestampPrefix },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
import("../config/config.js"),
|
import("../config/config.js"),
|
||||||
import("../gateway/server.js"),
|
import("../gateway/server.js"),
|
||||||
import("../gateway/ws-logging.js"),
|
import("../gateway/ws-logging.js"),
|
||||||
import("../globals.js"),
|
import("../globals.js"),
|
||||||
import("../runtime.js"),
|
import("../runtime.js"),
|
||||||
|
import("../logging.js"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
enableConsoleCapture();
|
||||||
|
setConsoleTimestampPrefix(true);
|
||||||
setVerbose(hasFlag(args, "--verbose"));
|
setVerbose(hasFlag(args, "--verbose"));
|
||||||
|
|
||||||
const wsLogRaw = (hasFlag(args, "--compact") ? "compact" : argValue(args, "--ws-log")) as
|
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);
|
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;
|
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 META_KEY = "memory_index_meta_v1";
|
||||||
const SNIPPET_MAX_CHARS = 700;
|
const SNIPPET_MAX_CHARS = 700;
|
||||||
const VECTOR_TABLE = "chunks_vec";
|
const VECTOR_TABLE = "chunks_vec";
|
||||||
const SESSION_DIRTY_DEBOUNCE_MS = 5000;
|
const SESSION_DIRTY_DEBOUNCE_MS = 5000;
|
||||||
const EMBEDDING_BATCH_MAX_TOKENS = 8000;
|
const EMBEDDING_BATCH_MAX_TOKENS = 8000;
|
||||||
const EMBEDDING_APPROX_CHARS_PER_TOKEN = 2;
|
const EMBEDDING_APPROX_CHARS_PER_TOKEN = 1;
|
||||||
|
|
||||||
const log = createSubsystemLogger("memory");
|
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;
|
if (this.syncing) return this.syncing;
|
||||||
this.syncing = this.runSync(params).finally(() => {
|
this.syncing = this.runSync(params).finally(() => {
|
||||||
this.syncing = null;
|
this.syncing = null;
|
||||||
@ -650,21 +666,46 @@ export class MemoryIndexManager {
|
|||||||
return this.sessionsDirty || needsFullReindex;
|
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 files = await listMemoryFiles(this.workspaceDir);
|
||||||
const fileEntries = await Promise.all(
|
const fileEntries = await Promise.all(
|
||||||
files.map(async (file) => buildFileEntry(file, this.workspaceDir)),
|
files.map(async (file) => buildFileEntry(file, this.workspaceDir)),
|
||||||
);
|
);
|
||||||
const activePaths = new Set(fileEntries.map((entry) => entry.path));
|
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) {
|
for (const entry of fileEntries) {
|
||||||
const record = this.db
|
const record = this.db
|
||||||
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
||||||
.get(entry.path, "memory") as { hash: string } | undefined;
|
.get(entry.path, "memory") as { hash: string } | undefined;
|
||||||
if (!params.needsFullReindex && record?.hash === entry.hash) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
await this.indexFile(entry, { source: "memory" });
|
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
|
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 files = await this.listSessionFiles();
|
||||||
const activePaths = new Set(files.map((file) => this.sessionPathForFile(file)));
|
const activePaths = new Set(files.map((file) => this.sessionPathForFile(file)));
|
||||||
const indexAll = params.needsFullReindex || this.sessionsDirtyFiles.size === 0;
|
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) {
|
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);
|
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
|
const record = this.db
|
||||||
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
||||||
.get(entry.path, "sessions") as { hash: string } | undefined;
|
.get(entry.path, "sessions") as { hash: string } | undefined;
|
||||||
if (!params.needsFullReindex && record?.hash === entry.hash) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
await this.indexFile(entry, { source: "sessions", content: entry.content });
|
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
|
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 vectorReady = await this.ensureVectorReady();
|
||||||
const meta = this.readMeta();
|
const meta = this.readMeta();
|
||||||
const needsFullReindex =
|
const needsFullReindex =
|
||||||
@ -729,12 +820,12 @@ export class MemoryIndexManager {
|
|||||||
const shouldSyncSessions = this.shouldSyncSessions(params, needsFullReindex);
|
const shouldSyncSessions = this.shouldSyncSessions(params, needsFullReindex);
|
||||||
|
|
||||||
if (shouldSyncMemory) {
|
if (shouldSyncMemory) {
|
||||||
await this.syncMemoryFiles({ needsFullReindex });
|
await this.syncMemoryFiles({ needsFullReindex, progress: progress ?? undefined });
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSyncSessions) {
|
if (shouldSyncSessions) {
|
||||||
await this.syncSessionFiles({ needsFullReindex });
|
await this.syncSessionFiles({ needsFullReindex, progress: progress ?? undefined });
|
||||||
this.sessionsDirty = false;
|
this.sessionsDirty = false;
|
||||||
this.sessionsDirtyFiles.clear();
|
this.sessionsDirtyFiles.clear();
|
||||||
} else if (needsFullReindex && this.sources.has("sessions")) {
|
} else if (needsFullReindex && this.sources.has("sessions")) {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
buildTelegramGroupFrom,
|
buildTelegramGroupFrom,
|
||||||
buildTelegramGroupPeerId,
|
buildTelegramGroupPeerId,
|
||||||
buildTypingThreadParams,
|
buildTypingThreadParams,
|
||||||
|
expandTextLinks,
|
||||||
normalizeForwardedContext,
|
normalizeForwardedContext,
|
||||||
describeReplyTarget,
|
describeReplyTarget,
|
||||||
extractTelegramLocation,
|
extractTelegramLocation,
|
||||||
@ -271,7 +272,8 @@ export const buildTelegramMessageContext = async ({
|
|||||||
|
|
||||||
const locationData = extractTelegramLocation(msg);
|
const locationData = extractTelegramLocation(msg);
|
||||||
const locationText = locationData ? formatLocationText(locationData) : undefined;
|
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();
|
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
|
||||||
if (!rawBody) rawBody = placeholder;
|
if (!rawBody) rawBody = placeholder;
|
||||||
if (!rawBody && allMedia.length === 0) return null;
|
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;
|
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 {
|
export function resolveTelegramReplyId(raw?: string): number | undefined {
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
const parsed = Number(raw);
|
const parsed = Number(raw);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user