diff --git a/src/embedder.ts b/src/embedder.ts index 51c7d01..27e59a2 100644 --- a/src/embedder.ts +++ b/src/embedder.ts @@ -1,4 +1,5 @@ import type { EmbedderHealth, EmbedderRetryConfig, EmbeddingConfig } from "./types.js"; +import { log } from "./logger.js"; export interface Embedder { readonly model: string; @@ -63,7 +64,7 @@ async function embedWithRetry( globalEmbedderHealth.lastError = null; if (globalEmbedderHealth.status === "degraded") { globalEmbedderHealth.status = "healthy"; - console.info(`[lancedb-opencode-pro] Embedder recovered, resuming normal mode`); + log("info", "Embedder recovered, resuming normal mode"); } return result; } catch (error) { @@ -78,18 +79,14 @@ async function embedWithRetry( const delay = Math.floor( retry.initialDelayMs * Math.pow(retry.backoffMultiplier, attempt - 1), ); - console.warn( - `[lancedb-opencode-pro] Embedder failed (attempt ${attempt}/${retry.maxAttempts}), retrying in ${delay}ms: ${lastError.message}`, - ); + log("warn", `Embedder failed (attempt ${attempt}/${retry.maxAttempts}), retrying in ${delay}ms: ${lastError.message}`); await sleep(delay); } } globalEmbedderHealth.status = "degraded"; globalEmbedderHealth.fallbackActive = true; - console.warn( - `[lancedb-opencode-pro] Embedder unavailable after ${retry.maxAttempts} attempts, falling back to BM25-only search`, - ); + log("warn", `Embedder unavailable after ${retry.maxAttempts} attempts, falling back to BM25-only search`); throw lastError; } @@ -218,9 +215,7 @@ export class OllamaEmbedder implements Embedder { } catch { const fb = fallbackDim(this.model); if (fb !== null) { - console.warn( - `[lancedb-opencode-pro] Ollama unreachable, using fallback dim ${fb} for model "${this.model}"`, - ); + log("warn", `Ollama unreachable, using fallback dim ${fb} for model "${this.model}"`); return fb; } throw new Error( @@ -296,9 +291,7 @@ export class OpenAIEmbedder implements Embedder { } catch { const fb = fallbackDim(this.model); if (fb !== null) { - console.warn( - `[lancedb-opencode-pro] OpenAI embedding probe failed, using fallback dim ${fb} for model "${this.model}"`, - ); + log("warn", `OpenAI embedding probe failed, using fallback dim ${fb} for model "${this.model}"`); return fb; } throw new Error( diff --git a/src/index.ts b/src/index.ts index e01f85d..75aef04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,12 @@ import { MemoryStore } from "./store.js"; import type { CaptureOutcome, CaptureSkipReason, EpisodicTaskRecord, FailureType, LastRecallSession, MemoryRuntimeConfig, PreferenceProfile, SearchResult, SuccessPattern, TaskState, TaskType, ValidationOutcome, ValidationType } from "./types.js"; import { validateEpisodicRecordArray } from "./types.js"; import { generateId } from "./utils.js"; +import { initLogger, log } from "./logger.js"; import { calculateInjectionLimit, createSummarizationConfig, summarizeContent } from "./summarize.js"; import { createMemoryTools, createFeedbackTools, createEpisodicTools, type ToolRuntimeState } from "./tools/index.js"; +const PLUGIN_VERSION = "0.7.0"; + const SCHEMA_VERSION = 1; // Task-type detection keywords @@ -71,6 +74,9 @@ function getCategoryWeights(taskType: TaskType, profiles: Record { + initLogger(input.client); + log("info", `Plugin v${PLUGIN_VERSION} initialized`); + const state = await createRuntimeState(input); const hooks: Hooks = { @@ -136,7 +142,7 @@ const plugin: Plugin = async (input) => { queryVector = await state.embedder.embed(query); } catch (error) { embedderFailed = true; - console.warn(`[lancedb-opencode-pro] embedding unavailable during recall: ${toErrorMessage(error)}`); + log("warn", `embedding unavailable during recall: ${toErrorMessage(error)}`); queryVector = []; } @@ -145,7 +151,7 @@ const plugin: Plugin = async (input) => { const effectiveBm25Weight = isFallback ? 1 : (state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight); if (isFallback) { - console.info(`[lancedb-opencode-pro] Using BM25-only search (embedder unavailable)`); + log("info", "Using BM25-only search (embedder unavailable)"); } const results = await state.store.search({ @@ -279,7 +285,7 @@ const plugin: Plugin = async (input) => { ); } } catch (error) { - console.warn(`[lancedb-opencode-pro] similar task recall failed: ${toErrorMessage(error)}`); + log("warn", `similar task recall failed: ${toErrorMessage(error)}`); } eventOutput.system.push(blocks.join("\n\n")); @@ -320,9 +326,7 @@ async function createRuntimeState(input: Parameters[0]): Promise, +): void { + if (_client?.app?.log) { + _client.app + .log({ + body: { + service: SERVICE_NAME, + level, + message, + ...(extra !== undefined ? { extra } : {}), + }, + }) + .catch(() => consoleFallback(level, message)); + return; + } + + consoleFallback(level, message); +} + +function consoleFallback(level: LogLevel, message: string): void { + const formatted = `[${SERVICE_NAME}] ${message}`; + switch (level) { + case "error": + console.error(formatted); + break; + case "warn": + console.warn(formatted); + break; + case "info": + console.info(formatted); + break; + default: + console.log(formatted); + break; + } +} diff --git a/src/tools/memory.ts b/src/tools/memory.ts index ca0c527..74408a6 100644 --- a/src/tools/memory.ts +++ b/src/tools/memory.ts @@ -2,6 +2,7 @@ import { tool } from "@opencode-ai/plugin"; import { deriveProjectScope, buildScopeFilter } from "../scope.js"; import { generateId } from "../utils.js"; import { getEmbedderHealth, type Embedder } from "../embedder.js"; +import { log } from "../logger.js"; import type { MemoryStore } from "../store.js"; import type { MemoryRuntimeConfig, MemoryCategory, CitationStatus, ValidationOutcome } from "../types.js"; @@ -68,7 +69,7 @@ export function createMemoryTools(state: ToolRuntimeState) { const effectiveBm25Weight = isFallback ? 1 : (state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight); if (isFallback) { - console.info(`[lancedb-opencode-pro] Using BM25-only search (embedder unavailable)`); + log("info", "Using BM25-only search (embedder unavailable)"); } const results = await state.store.search({ diff --git a/test/config.test.ts b/test/config.test.ts index 5a12442..e49b941 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -2,11 +2,15 @@ import assert from "node:assert/strict"; import test from "node:test"; import { resolveMemoryConfig } from "../src/config.js"; -async function withPatchedEnv(values: Record, run: () => T): Promise { +async function withPatchedEnv(values: Record, run: () => T): Promise { const oldValues: Record = {}; for (const key of Object.keys(values)) { oldValues[key] = process.env[key]; - process.env[key] = values[key]; + if (values[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = values[key] as string; + } } try { return run(); diff --git a/test/regression/plugin.test.ts b/test/regression/plugin.test.ts index d7a02ce..670bc2b 100644 --- a/test/regression/plugin.test.ts +++ b/test/regression/plugin.test.ts @@ -57,11 +57,15 @@ async function withPatchedFetch(run: () => Promise): Promise { } } -async function withPatchedEnv(values: Record, run: () => Promise): Promise { +async function withPatchedEnv(values: Record, run: () => Promise): Promise { const previous = new Map(); for (const [key, value] of Object.entries(values)) { previous.set(key, process.env[key]); - process.env[key] = value; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } } try { @@ -350,6 +354,8 @@ test("resolveMemoryConfig fails fast for openai without model", async () => { LANCEDB_OPENCODE_PRO_EMBEDDING_PROVIDER: "openai", LANCEDB_OPENCODE_PRO_OPENAI_API_KEY: "test-openai-api-key", LANCEDB_OPENCODE_PRO_SKIP_SIDECAR: "true", + LANCEDB_OPENCODE_PRO_EMBEDDING_MODEL: undefined, + LANCEDB_OPENCODE_PRO_OPENAI_MODEL: undefined, }, async () => { assert.throws(