chore: remove verified dead code paths
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export { serveAcpGateway } from "./server.js";
|
||||
export { createInMemorySessionStore } from "./session.js";
|
||||
export type { AcpSessionStore } from "./session.js";
|
||||
export type { AcpServerOptions } from "./types.js";
|
||||
@@ -1,68 +0,0 @@
|
||||
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
||||
import type { Mock } from "vitest";
|
||||
import {
|
||||
handleToolExecutionEnd,
|
||||
handleToolExecutionStart,
|
||||
} from "./pi-embedded-subscribe.handlers.tools.js";
|
||||
import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js";
|
||||
import type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.types.js";
|
||||
|
||||
/**
|
||||
* Narrowed params type that omits the `session` class instance (never accessed
|
||||
* by the handler paths under test).
|
||||
*/
|
||||
type TestParams = Omit<SubscribeEmbeddedPiSessionParams, "session">;
|
||||
|
||||
/**
|
||||
* The subset of {@link EmbeddedPiSubscribeContext} that the media-emission
|
||||
* tests actually populate. Using this avoids the need for `as unknown as`
|
||||
* double-assertion in every mock factory.
|
||||
*/
|
||||
export type MockEmbeddedContext = Omit<EmbeddedPiSubscribeContext, "params"> & {
|
||||
params: TestParams;
|
||||
};
|
||||
|
||||
/** Type-safe bridge: narrows parameter type so callers avoid assertions. */
|
||||
function asFullContext(ctx: MockEmbeddedContext): EmbeddedPiSubscribeContext {
|
||||
return ctx as unknown as EmbeddedPiSubscribeContext;
|
||||
}
|
||||
|
||||
/** Typed wrapper around {@link handleToolExecutionStart}. */
|
||||
export function callToolExecutionStart(
|
||||
ctx: MockEmbeddedContext,
|
||||
evt: AgentEvent & { toolName: string; toolCallId: string; args: unknown },
|
||||
): Promise<void> {
|
||||
return handleToolExecutionStart(asFullContext(ctx), evt);
|
||||
}
|
||||
|
||||
/** Typed wrapper around {@link handleToolExecutionEnd}. */
|
||||
export function callToolExecutionEnd(
|
||||
ctx: MockEmbeddedContext,
|
||||
evt: AgentEvent & {
|
||||
toolName: string;
|
||||
toolCallId: string;
|
||||
isError: boolean;
|
||||
result?: unknown;
|
||||
},
|
||||
): Promise<void> {
|
||||
return handleToolExecutionEnd(asFullContext(ctx), evt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a mock-call argument is an object containing `mediaUrls`
|
||||
* but NOT `text` (i.e. a "direct media" emission).
|
||||
*/
|
||||
export function isDirectMediaCall(call: unknown[]): boolean {
|
||||
const arg = call[0];
|
||||
if (!arg || typeof arg !== "object") {
|
||||
return false;
|
||||
}
|
||||
return "mediaUrls" in arg && !("text" in arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a vi.fn() mock's call log to only direct-media emissions.
|
||||
*/
|
||||
export function filterDirectMediaCalls(mock: Mock): unknown[][] {
|
||||
return mock.mock.calls.filter(isDirectMediaCall);
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../../gateway/call.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
type NodeSummary = {
|
||||
nodeId: string;
|
||||
displayName?: string;
|
||||
platform?: string;
|
||||
deviceFamily?: string;
|
||||
remoteIp?: string;
|
||||
connected?: boolean;
|
||||
};
|
||||
|
||||
const PTT_COMMANDS: Record<string, string> = {
|
||||
start: "talk.ptt.start",
|
||||
stop: "talk.ptt.stop",
|
||||
once: "talk.ptt.once",
|
||||
cancel: "talk.ptt.cancel",
|
||||
};
|
||||
|
||||
function normalizeNodeKey(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+/, "")
|
||||
.replace(/-+$/, "");
|
||||
}
|
||||
|
||||
function isIOSNode(node: NodeSummary): boolean {
|
||||
const platform = node.platform?.toLowerCase() ?? "";
|
||||
const family = node.deviceFamily?.toLowerCase() ?? "";
|
||||
return (
|
||||
platform.startsWith("ios") ||
|
||||
family.includes("iphone") ||
|
||||
family.includes("ipad") ||
|
||||
family.includes("ios")
|
||||
);
|
||||
}
|
||||
|
||||
async function loadNodes(cfg: OpenClawConfig): Promise<NodeSummary[]> {
|
||||
try {
|
||||
const res = await callGateway<{ nodes?: NodeSummary[] }>({
|
||||
method: "node.list",
|
||||
params: {},
|
||||
config: cfg,
|
||||
});
|
||||
return Array.isArray(res.nodes) ? res.nodes : [];
|
||||
} catch {
|
||||
const res = await callGateway<{ pending?: unknown[]; paired?: NodeSummary[] }>({
|
||||
method: "node.pair.list",
|
||||
params: {},
|
||||
config: cfg,
|
||||
});
|
||||
return Array.isArray(res.paired) ? res.paired : [];
|
||||
}
|
||||
}
|
||||
|
||||
function describeNodes(nodes: NodeSummary[]) {
|
||||
return nodes
|
||||
.map((node) => node.displayName || node.remoteIp || node.nodeId)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function resolveNodeId(nodes: NodeSummary[], query?: string): string {
|
||||
const trimmed = String(query ?? "").trim();
|
||||
if (trimmed) {
|
||||
const qNorm = normalizeNodeKey(trimmed);
|
||||
const matches = nodes.filter((node) => {
|
||||
if (node.nodeId === trimmed) {
|
||||
return true;
|
||||
}
|
||||
if (typeof node.remoteIp === "string" && node.remoteIp === trimmed) {
|
||||
return true;
|
||||
}
|
||||
const name = typeof node.displayName === "string" ? node.displayName : "";
|
||||
if (name && normalizeNodeKey(name) === qNorm) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.length >= 6 && node.nodeId.startsWith(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matches.length === 1) {
|
||||
return matches[0].nodeId;
|
||||
}
|
||||
const known = describeNodes(nodes);
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`unknown node: ${trimmed}${known ? ` (known: ${known})` : ""}`);
|
||||
}
|
||||
throw new Error(
|
||||
`ambiguous node: ${trimmed} (matches: ${matches
|
||||
.map((node) => node.displayName || node.remoteIp || node.nodeId)
|
||||
.join(", ")})`,
|
||||
);
|
||||
}
|
||||
|
||||
const iosNodes = nodes.filter(isIOSNode);
|
||||
const iosConnected = iosNodes.filter((node) => node.connected);
|
||||
const iosCandidates = iosConnected.length > 0 ? iosConnected : iosNodes;
|
||||
if (iosCandidates.length === 1) {
|
||||
return iosCandidates[0].nodeId;
|
||||
}
|
||||
if (iosCandidates.length > 1) {
|
||||
throw new Error(
|
||||
`multiple iOS nodes found (${describeNodes(iosCandidates)}); specify node=<id>`,
|
||||
);
|
||||
}
|
||||
|
||||
const connected = nodes.filter((node) => node.connected);
|
||||
const fallback = connected.length > 0 ? connected : nodes;
|
||||
if (fallback.length === 1) {
|
||||
return fallback[0].nodeId;
|
||||
}
|
||||
|
||||
const known = describeNodes(nodes);
|
||||
throw new Error(`node required${known ? ` (known: ${known})` : ""}`);
|
||||
}
|
||||
|
||||
function parsePTTArgs(commandBody: string) {
|
||||
const tokens = commandBody.trim().split(/\s+/).slice(1);
|
||||
let action: string | undefined;
|
||||
let node: string | undefined;
|
||||
for (const token of tokens) {
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (token.toLowerCase().startsWith("node=")) {
|
||||
node = token.slice("node=".length);
|
||||
continue;
|
||||
}
|
||||
if (!action) {
|
||||
action = token;
|
||||
}
|
||||
}
|
||||
return { action, node };
|
||||
}
|
||||
|
||||
function buildPTTHelpText() {
|
||||
return [
|
||||
"Usage: /ptt <start|stop|once|cancel> [node=<id>]",
|
||||
"Example: /ptt once node=iphone",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export const handlePTTCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) {
|
||||
return null;
|
||||
}
|
||||
const { command, cfg } = params;
|
||||
const normalized = command.commandBodyNormalized.trim();
|
||||
if (!normalized.startsWith("/ptt")) {
|
||||
return null;
|
||||
}
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(`Ignoring /ptt from unauthorized sender: ${command.senderId || "<unknown>"}`);
|
||||
return { shouldContinue: false, reply: { text: "PTT requires an authorized sender." } };
|
||||
}
|
||||
|
||||
const parsed = parsePTTArgs(normalized);
|
||||
const actionKey = parsed.action?.trim().toLowerCase() ?? "";
|
||||
const commandId = PTT_COMMANDS[actionKey];
|
||||
if (!commandId) {
|
||||
return { shouldContinue: false, reply: { text: buildPTTHelpText() } };
|
||||
}
|
||||
|
||||
try {
|
||||
const nodes = await loadNodes(cfg);
|
||||
const nodeId = resolveNodeId(nodes, parsed.node);
|
||||
const invokeParams: Record<string, unknown> = {
|
||||
nodeId,
|
||||
command: commandId,
|
||||
params: {},
|
||||
idempotencyKey: randomIdempotencyKey(),
|
||||
timeoutMs: 15_000,
|
||||
};
|
||||
const res = await callGateway<{
|
||||
ok?: boolean;
|
||||
payload?: Record<string, unknown>;
|
||||
command?: string;
|
||||
nodeId?: string;
|
||||
}>({
|
||||
method: "node.invoke",
|
||||
params: invokeParams,
|
||||
config: cfg,
|
||||
});
|
||||
const payload = res.payload && typeof res.payload === "object" ? res.payload : {};
|
||||
|
||||
const lines = [`PTT ${actionKey} → ${nodeId}`];
|
||||
if (typeof payload.status === "string") {
|
||||
lines.push(`status: ${payload.status}`);
|
||||
}
|
||||
if (typeof payload.captureId === "string") {
|
||||
lines.push(`captureId: ${payload.captureId}`);
|
||||
}
|
||||
if (typeof payload.transcript === "string" && payload.transcript.trim()) {
|
||||
lines.push(`transcript: ${payload.transcript}`);
|
||||
}
|
||||
|
||||
return { shouldContinue: false, reply: { text: lines.join("\n") } };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { shouldContinue: false, reply: { text: `PTT failed: ${message}` } };
|
||||
}
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { monitorDiscordProvider } from "./monitor.js";
|
||||
export { sendMessageDiscord, sendPollDiscord } from "./send.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
export { monitorIMessageProvider } from "./monitor.js";
|
||||
export { probeIMessage } from "./probe.js";
|
||||
export { sendMessageIMessage } from "./send.js";
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
export type LineHttpRequestHandler = (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
) => Promise<void> | void;
|
||||
|
||||
type RegisterLineHttpHandlerArgs = {
|
||||
path?: string | null;
|
||||
handler: LineHttpRequestHandler;
|
||||
log?: (message: string) => void;
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
const lineHttpRoutes = new Map<string, LineHttpRequestHandler>();
|
||||
|
||||
export function normalizeLineWebhookPath(path?: string | null): string {
|
||||
const trimmed = path?.trim();
|
||||
if (!trimmed) {
|
||||
return "/line/webhook";
|
||||
}
|
||||
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||
}
|
||||
|
||||
export function registerLineHttpHandler(params: RegisterLineHttpHandlerArgs): () => void {
|
||||
const normalizedPath = normalizeLineWebhookPath(params.path);
|
||||
if (lineHttpRoutes.has(normalizedPath)) {
|
||||
const suffix = params.accountId ? ` for account "${params.accountId}"` : "";
|
||||
params.log?.(`line: webhook path ${normalizedPath} already registered${suffix}`);
|
||||
return () => {};
|
||||
}
|
||||
lineHttpRoutes.set(normalizedPath, params.handler);
|
||||
return () => {
|
||||
lineHttpRoutes.delete(normalizedPath);
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleLineHttpRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<boolean> {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const handler = lineHttpRoutes.get(url.pathname);
|
||||
if (!handler) {
|
||||
return false;
|
||||
}
|
||||
await handler(req, res);
|
||||
return true;
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
export {
|
||||
createLineBot,
|
||||
createLineWebhookCallback,
|
||||
type LineBot,
|
||||
type LineBotOptions,
|
||||
} from "./bot.js";
|
||||
export {
|
||||
monitorLineProvider,
|
||||
getLineRuntimeState,
|
||||
type MonitorLineProviderOptions,
|
||||
type LineProviderMonitor,
|
||||
} from "./monitor.js";
|
||||
export {
|
||||
sendMessageLine,
|
||||
pushMessageLine,
|
||||
pushMessagesLine,
|
||||
replyMessageLine,
|
||||
createImageMessage,
|
||||
createLocationMessage,
|
||||
createFlexMessage,
|
||||
createQuickReplyItems,
|
||||
createTextMessageWithQuickReplies,
|
||||
showLoadingAnimation,
|
||||
getUserProfile,
|
||||
getUserDisplayName,
|
||||
pushImageMessage,
|
||||
pushLocationMessage,
|
||||
pushFlexMessage,
|
||||
pushTemplateMessage,
|
||||
pushTextMessageWithQuickReplies,
|
||||
} from "./send.js";
|
||||
export {
|
||||
startLineWebhook,
|
||||
createLineWebhookMiddleware,
|
||||
type LineWebhookOptions,
|
||||
type StartLineWebhookOptions,
|
||||
} from "./webhook.js";
|
||||
export {
|
||||
handleLineHttpRequest,
|
||||
registerLineHttpHandler,
|
||||
normalizeLineWebhookPath,
|
||||
} from "./http-registry.js";
|
||||
export {
|
||||
resolveLineAccount,
|
||||
listLineAccountIds,
|
||||
resolveDefaultLineAccountId,
|
||||
normalizeAccountId,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
} from "./accounts.js";
|
||||
export { probeLineBot } from "./probe.js";
|
||||
export { downloadLineMedia } from "./download.js";
|
||||
export { LineConfigSchema, type LineConfigSchemaType } from "./config-schema.js";
|
||||
export { buildLineMessageContext } from "./bot-message-context.js";
|
||||
export { handleLineWebhookEvents, type LineHandlerContext } from "./bot-handlers.js";
|
||||
|
||||
// Flex Message templates
|
||||
export {
|
||||
createInfoCard,
|
||||
createListCard,
|
||||
createImageCard,
|
||||
createActionCard,
|
||||
createCarousel,
|
||||
createNotificationBubble,
|
||||
createReceiptCard,
|
||||
createEventCard,
|
||||
createMediaPlayerCard,
|
||||
createAppleTvRemoteCard,
|
||||
createDeviceControlCard,
|
||||
toFlexMessage,
|
||||
type ListItem,
|
||||
type CardAction,
|
||||
type FlexContainer,
|
||||
type FlexBubble,
|
||||
type FlexCarousel,
|
||||
} from "./flex-templates.js";
|
||||
|
||||
// Markdown to LINE conversion
|
||||
export {
|
||||
processLineMessage,
|
||||
hasMarkdownToConvert,
|
||||
stripMarkdown,
|
||||
extractMarkdownTables,
|
||||
extractCodeBlocks,
|
||||
extractLinks,
|
||||
convertTableToFlexBubble,
|
||||
convertCodeBlockToFlexBubble,
|
||||
convertLinksToFlexBubble,
|
||||
type ProcessedLineMessage,
|
||||
type MarkdownTable,
|
||||
type CodeBlock,
|
||||
type MarkdownLink,
|
||||
} from "./markdown-to-line.js";
|
||||
|
||||
// Rich Menu operations
|
||||
export {
|
||||
createRichMenu,
|
||||
uploadRichMenuImage,
|
||||
setDefaultRichMenu,
|
||||
cancelDefaultRichMenu,
|
||||
getDefaultRichMenuId,
|
||||
linkRichMenuToUser,
|
||||
linkRichMenuToUsers,
|
||||
unlinkRichMenuFromUser,
|
||||
unlinkRichMenuFromUsers,
|
||||
getRichMenuIdOfUser,
|
||||
getRichMenuList,
|
||||
getRichMenu,
|
||||
deleteRichMenu,
|
||||
createRichMenuAlias,
|
||||
deleteRichMenuAlias,
|
||||
createGridLayout,
|
||||
messageAction,
|
||||
uriAction,
|
||||
postbackAction,
|
||||
datetimePickerAction,
|
||||
createDefaultMenuConfig,
|
||||
type CreateRichMenuParams,
|
||||
type RichMenuSize,
|
||||
type RichMenuAreaRequest,
|
||||
} from "./rich-menu.js";
|
||||
|
||||
// Template messages (Button, Confirm, Carousel)
|
||||
export {
|
||||
createConfirmTemplate,
|
||||
createButtonTemplate,
|
||||
createTemplateCarousel,
|
||||
createCarouselColumn,
|
||||
createImageCarousel,
|
||||
createImageCarouselColumn,
|
||||
createYesNoConfirm,
|
||||
createButtonMenu,
|
||||
createLinkMenu,
|
||||
createProductCarousel,
|
||||
messageAction as templateMessageAction,
|
||||
uriAction as templateUriAction,
|
||||
postbackAction as templatePostbackAction,
|
||||
datetimePickerAction as templateDatetimePickerAction,
|
||||
type TemplateMessage,
|
||||
type ConfirmTemplate,
|
||||
type ButtonsTemplate,
|
||||
type CarouselTemplate,
|
||||
type CarouselColumn,
|
||||
} from "./template-messages.js";
|
||||
|
||||
export type {
|
||||
LineConfig,
|
||||
LineAccountConfig,
|
||||
LineGroupConfig,
|
||||
ResolvedLineAccount,
|
||||
LineTokenSource,
|
||||
LineMessageType,
|
||||
LineWebhookContext,
|
||||
LineSendResult,
|
||||
LineProbeResult,
|
||||
} from "./types.js";
|
||||
@@ -1,4 +0,0 @@
|
||||
export { applyLinkUnderstanding } from "./apply.js";
|
||||
export { extractLinksFromMessage } from "./detect.js";
|
||||
export { formatLinkUnderstandingBody } from "./format.js";
|
||||
export { runLinkUnderstanding } from "./runner.js";
|
||||
@@ -1,9 +0,0 @@
|
||||
export { applyMediaUnderstanding } from "./apply.js";
|
||||
export { formatMediaUnderstandingBody } from "./format.js";
|
||||
export { resolveMediaUnderstandingScope } from "./scope.js";
|
||||
export type {
|
||||
MediaAttachment,
|
||||
MediaUnderstandingOutput,
|
||||
MediaUnderstandingProvider,
|
||||
MediaUnderstandingKind,
|
||||
} from "./types.js";
|
||||
@@ -1,19 +0,0 @@
|
||||
function normalizeHeaderName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function fingerprintHeaderNames(headers: Record<string, string> | undefined): string[] {
|
||||
if (!headers) {
|
||||
return [];
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const key of Object.keys(headers)) {
|
||||
const normalized = normalizeHeaderName(key);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
out.push(normalized);
|
||||
}
|
||||
out.sort((a, b) => a.localeCompare(b));
|
||||
return out;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import { fingerprintHeaderNames } from "./headers-fingerprint.js";
|
||||
import { hashText } from "./internal.js";
|
||||
|
||||
export function computeMemoryManagerCacheKey(params: {
|
||||
agentId: string;
|
||||
workspaceDir: string;
|
||||
settings: ResolvedMemorySearchConfig;
|
||||
}): string {
|
||||
const settings = params.settings;
|
||||
const fingerprint = hashText(
|
||||
JSON.stringify({
|
||||
enabled: settings.enabled,
|
||||
sources: [...settings.sources].toSorted((a, b) => a.localeCompare(b)),
|
||||
extraPaths: [...settings.extraPaths].toSorted((a, b) => a.localeCompare(b)),
|
||||
provider: settings.provider,
|
||||
model: settings.model,
|
||||
fallback: settings.fallback,
|
||||
local: {
|
||||
modelPath: settings.local.modelPath,
|
||||
modelCacheDir: settings.local.modelCacheDir,
|
||||
},
|
||||
remote: settings.remote
|
||||
? {
|
||||
baseUrl: settings.remote.baseUrl,
|
||||
headerNames: fingerprintHeaderNames(settings.remote.headers),
|
||||
batch: settings.remote.batch
|
||||
? {
|
||||
enabled: settings.remote.batch.enabled,
|
||||
wait: settings.remote.batch.wait,
|
||||
concurrency: settings.remote.batch.concurrency,
|
||||
pollIntervalMs: settings.remote.batch.pollIntervalMs,
|
||||
timeoutMinutes: settings.remote.batch.timeoutMinutes,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
experimental: settings.experimental,
|
||||
store: {
|
||||
driver: settings.store.driver,
|
||||
path: settings.store.path,
|
||||
vector: {
|
||||
enabled: settings.store.vector.enabled,
|
||||
extensionPath: settings.store.vector.extensionPath,
|
||||
},
|
||||
},
|
||||
chunking: settings.chunking,
|
||||
sync: settings.sync,
|
||||
query: settings.query,
|
||||
cache: settings.cache,
|
||||
}),
|
||||
);
|
||||
return `${params.agentId}:${params.workspaceDir}:${fingerprint}`;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Deprecated: use ./batch-openai.js
|
||||
export * from "./batch-openai.js";
|
||||
@@ -1,33 +0,0 @@
|
||||
import { fingerprintHeaderNames } from "./headers-fingerprint.js";
|
||||
import { hashText } from "./internal.js";
|
||||
|
||||
export function computeEmbeddingProviderKey(params: {
|
||||
providerId: string;
|
||||
providerModel: string;
|
||||
openAi?: { baseUrl: string; model: string; headers: Record<string, string> };
|
||||
gemini?: { baseUrl: string; model: string; headers: Record<string, string> };
|
||||
}): string {
|
||||
if (params.openAi) {
|
||||
const headerNames = fingerprintHeaderNames(params.openAi.headers);
|
||||
return hashText(
|
||||
JSON.stringify({
|
||||
provider: "openai",
|
||||
baseUrl: params.openAi.baseUrl,
|
||||
model: params.openAi.model,
|
||||
headerNames,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (params.gemini) {
|
||||
const headerNames = fingerprintHeaderNames(params.gemini.headers);
|
||||
return hashText(
|
||||
JSON.stringify({
|
||||
provider: "gemini",
|
||||
baseUrl: params.gemini.baseUrl,
|
||||
model: params.gemini.model,
|
||||
headerNames,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return hashText(JSON.stringify({ provider: params.providerId, model: params.providerModel }));
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
type SyncProgress = {
|
||||
completed: number;
|
||||
total: number;
|
||||
report: (update: { completed: number; total: number; label?: string }) => void;
|
||||
};
|
||||
|
||||
function tickProgress(progress: SyncProgress | undefined): void {
|
||||
if (!progress) {
|
||||
return;
|
||||
}
|
||||
progress.completed += 1;
|
||||
progress.report({
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
});
|
||||
}
|
||||
|
||||
export async function indexFileEntryIfChanged<
|
||||
TEntry extends { path: string; hash: string },
|
||||
>(params: {
|
||||
db: DatabaseSync;
|
||||
source: string;
|
||||
needsFullReindex: boolean;
|
||||
entry: TEntry;
|
||||
indexFile: (entry: TEntry) => Promise<void>;
|
||||
progress?: SyncProgress;
|
||||
}): Promise<void> {
|
||||
const record = params.db
|
||||
.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`)
|
||||
.get(params.entry.path, params.source) as { hash: string } | undefined;
|
||||
if (!params.needsFullReindex && record?.hash === params.entry.hash) {
|
||||
tickProgress(params.progress);
|
||||
return;
|
||||
}
|
||||
await params.indexFile(params.entry);
|
||||
tickProgress(params.progress);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { buildFileEntry, listMemoryFiles, type MemoryFileEntry } from "./internal.js";
|
||||
import { indexFileEntryIfChanged } from "./sync-index.js";
|
||||
import type { SyncProgressState } from "./sync-progress.js";
|
||||
import { bumpSyncProgressTotal } from "./sync-progress.js";
|
||||
import { deleteStaleIndexedPaths } from "./sync-stale.js";
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
export async function syncMemoryFiles(params: {
|
||||
workspaceDir: string;
|
||||
extraPaths?: string[];
|
||||
db: DatabaseSync;
|
||||
needsFullReindex: boolean;
|
||||
progress?: SyncProgressState;
|
||||
batchEnabled: boolean;
|
||||
concurrency: number;
|
||||
runWithConcurrency: <T>(tasks: Array<() => Promise<T>>, concurrency: number) => Promise<T[]>;
|
||||
indexFile: (entry: MemoryFileEntry) => Promise<void>;
|
||||
vectorTable: string;
|
||||
ftsTable: string;
|
||||
ftsEnabled: boolean;
|
||||
ftsAvailable: boolean;
|
||||
model: string;
|
||||
}) {
|
||||
const files = await listMemoryFiles(params.workspaceDir, params.extraPaths);
|
||||
const fileEntries = (
|
||||
await Promise.all(files.map(async (file) => buildFileEntry(file, params.workspaceDir)))
|
||||
).filter((entry): entry is MemoryFileEntry => entry !== null);
|
||||
|
||||
log.debug("memory sync: indexing memory files", {
|
||||
files: fileEntries.length,
|
||||
needsFullReindex: params.needsFullReindex,
|
||||
batch: params.batchEnabled,
|
||||
concurrency: params.concurrency,
|
||||
});
|
||||
|
||||
const activePaths = new Set(fileEntries.map((entry) => entry.path));
|
||||
bumpSyncProgressTotal(
|
||||
params.progress,
|
||||
fileEntries.length,
|
||||
params.batchEnabled ? "Indexing memory files (batch)..." : "Indexing memory files…",
|
||||
);
|
||||
|
||||
const tasks = fileEntries.map((entry) => async () => {
|
||||
await indexFileEntryIfChanged({
|
||||
db: params.db,
|
||||
source: "memory",
|
||||
needsFullReindex: params.needsFullReindex,
|
||||
entry,
|
||||
indexFile: params.indexFile,
|
||||
progress: params.progress,
|
||||
});
|
||||
});
|
||||
|
||||
await params.runWithConcurrency(tasks, params.concurrency);
|
||||
deleteStaleIndexedPaths({
|
||||
db: params.db,
|
||||
source: "memory",
|
||||
activePaths,
|
||||
vectorTable: params.vectorTable,
|
||||
ftsTable: params.ftsTable,
|
||||
ftsEnabled: params.ftsEnabled,
|
||||
ftsAvailable: params.ftsAvailable,
|
||||
model: params.model,
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
export type SyncProgressState = {
|
||||
completed: number;
|
||||
total: number;
|
||||
label?: string;
|
||||
report: (update: { completed: number; total: number; label?: string }) => void;
|
||||
};
|
||||
|
||||
export function bumpSyncProgressTotal(
|
||||
progress: SyncProgressState | undefined,
|
||||
delta: number,
|
||||
label?: string,
|
||||
) {
|
||||
if (!progress) {
|
||||
return;
|
||||
}
|
||||
progress.total += delta;
|
||||
progress.report({
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
export function bumpSyncProgressCompleted(
|
||||
progress: SyncProgressState | undefined,
|
||||
delta = 1,
|
||||
label?: string,
|
||||
) {
|
||||
if (!progress) {
|
||||
return;
|
||||
}
|
||||
progress.completed += delta;
|
||||
progress.report({
|
||||
completed: progress.completed,
|
||||
total: progress.total,
|
||||
label,
|
||||
});
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { SessionFileEntry } from "./session-files.js";
|
||||
import {
|
||||
buildSessionEntry,
|
||||
listSessionFilesForAgent,
|
||||
sessionPathForFile,
|
||||
} from "./session-files.js";
|
||||
import { indexFileEntryIfChanged } from "./sync-index.js";
|
||||
import type { SyncProgressState } from "./sync-progress.js";
|
||||
import { bumpSyncProgressCompleted, bumpSyncProgressTotal } from "./sync-progress.js";
|
||||
import { deleteStaleIndexedPaths } from "./sync-stale.js";
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
export async function syncSessionFiles(params: {
|
||||
agentId: string;
|
||||
db: DatabaseSync;
|
||||
needsFullReindex: boolean;
|
||||
progress?: SyncProgressState;
|
||||
batchEnabled: boolean;
|
||||
concurrency: number;
|
||||
runWithConcurrency: <T>(tasks: Array<() => Promise<T>>, concurrency: number) => Promise<T[]>;
|
||||
indexFile: (entry: SessionFileEntry) => Promise<void>;
|
||||
vectorTable: string;
|
||||
ftsTable: string;
|
||||
ftsEnabled: boolean;
|
||||
ftsAvailable: boolean;
|
||||
model: string;
|
||||
dirtyFiles: Set<string>;
|
||||
}) {
|
||||
const files = await listSessionFilesForAgent(params.agentId);
|
||||
const activePaths = new Set(files.map((file) => sessionPathForFile(file)));
|
||||
const indexAll = params.needsFullReindex || params.dirtyFiles.size === 0;
|
||||
|
||||
log.debug("memory sync: indexing session files", {
|
||||
files: files.length,
|
||||
indexAll,
|
||||
dirtyFiles: params.dirtyFiles.size,
|
||||
batch: params.batchEnabled,
|
||||
concurrency: params.concurrency,
|
||||
});
|
||||
|
||||
bumpSyncProgressTotal(
|
||||
params.progress,
|
||||
files.length,
|
||||
params.batchEnabled ? "Indexing session files (batch)..." : "Indexing session files…",
|
||||
);
|
||||
|
||||
const tasks = files.map((absPath) => async () => {
|
||||
if (!indexAll && !params.dirtyFiles.has(absPath)) {
|
||||
bumpSyncProgressCompleted(params.progress);
|
||||
return;
|
||||
}
|
||||
const entry = await buildSessionEntry(absPath);
|
||||
if (!entry) {
|
||||
bumpSyncProgressCompleted(params.progress);
|
||||
return;
|
||||
}
|
||||
await indexFileEntryIfChanged({
|
||||
db: params.db,
|
||||
source: "sessions",
|
||||
needsFullReindex: params.needsFullReindex,
|
||||
entry,
|
||||
indexFile: params.indexFile,
|
||||
progress: params.progress,
|
||||
});
|
||||
});
|
||||
|
||||
await params.runWithConcurrency(tasks, params.concurrency);
|
||||
deleteStaleIndexedPaths({
|
||||
db: params.db,
|
||||
source: "sessions",
|
||||
activePaths,
|
||||
vectorTable: params.vectorTable,
|
||||
ftsTable: params.ftsTable,
|
||||
ftsEnabled: params.ftsEnabled,
|
||||
ftsAvailable: params.ftsAvailable,
|
||||
model: params.model,
|
||||
});
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
export function deleteStaleIndexedPaths(params: {
|
||||
db: DatabaseSync;
|
||||
source: string;
|
||||
activePaths: Set<string>;
|
||||
vectorTable: string;
|
||||
ftsTable: string;
|
||||
ftsEnabled: boolean;
|
||||
ftsAvailable: boolean;
|
||||
model: string;
|
||||
}) {
|
||||
const staleRows = params.db
|
||||
.prepare(`SELECT path FROM files WHERE source = ?`)
|
||||
.all(params.source) as Array<{ path: string }>;
|
||||
|
||||
for (const stale of staleRows) {
|
||||
if (params.activePaths.has(stale.path)) {
|
||||
continue;
|
||||
}
|
||||
params.db
|
||||
.prepare(`DELETE FROM files WHERE path = ? AND source = ?`)
|
||||
.run(stale.path, params.source);
|
||||
try {
|
||||
params.db
|
||||
.prepare(
|
||||
`DELETE FROM ${params.vectorTable} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`,
|
||||
)
|
||||
.run(stale.path, params.source);
|
||||
} catch {}
|
||||
params.db
|
||||
.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`)
|
||||
.run(stale.path, params.source);
|
||||
if (params.ftsEnabled && params.ftsAvailable) {
|
||||
try {
|
||||
params.db
|
||||
.prepare(`DELETE FROM ${params.ftsTable} WHERE path = ? AND source = ? AND model = ?`)
|
||||
.run(stale.path, params.source, params.model);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user