Skip to content

fix: apply repository quality fixes#7

Merged
ZeR020 merged 1 commit into
mainfrom
audit-full-repo-2
May 6, 2026
Merged

fix: apply repository quality fixes#7
ZeR020 merged 1 commit into
mainfrom
audit-full-repo-2

Conversation

@ZeR020

@ZeR020 ZeR020 commented May 6, 2026

Copy link
Copy Markdown
Owner

Audit-Only PR — DO NOT MERGE

This PR was created to trigger a comprehensive CodeRabbit AI review of the entire opencode-mem0 codebase.

Scope (83 files)

  • 56 source files in src/
  • 22 test files in tests/
  • 3 scripts in scripts/
  • Config files

Review Focus

  1. 🔒 Security — injection risks, path traversal, data exposure
  2. 🏷️ Type Safetyany casts, missing null checks, unsafe assertions
  3. 🛡️ Error Handling — unhandled rejections, missing try/catch, silent failures
  4. 💧 Resource Leaks — unclosed DB connections, file handles, event listeners
  5. 🐛 Logic Bugs — race conditions, off-by-one, async/await issues
  6. 🪟 Cross-Platform — Windows path handling, shell command safety
  7. 🧪 Tests — coverage gaps, flaky tests, mock leaks
  8. Performance — N+1 queries, memory leaks, inefficient algorithms

Runtime Context

  • Bun 1.x (primary) / Node.js 20+ (fallback)
  • Windows/Linux/macOS
  • SQLite: bun:sqlite || better-sqlite3
  • HTTP: Bun.serve() || node:http

@coderabbitai Please perform a full audit of all changes in this PR.

Summary by CodeRabbit

  • Chores
    • Internal codebase maintenance and instrumentation updates with no impact on user-facing functionality or features.

Copilot AI review requested due to automatic review settings May 6, 2026 15:44
@ZeR020

ZeR020 commented May 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please perform a comprehensive full-repo audit focusing on security, type safety, error handling, resource leaks, logic bugs, cross-platform issues, and test coverage. Review all 83 files thoroughly.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds a single-line non-functional audit marker comment (// AUDIT_MARKER) to 72 files across the codebase spanning scripts, source modules, and test files. No behavioral, API, or logic changes are introduced.

Changes

Audit Marker Additions

Layer / File(s) Summary
Build & Script Markers
scripts/build.mjs, scripts/migrate-tests.mjs, scripts/migrate-v1-to-v2.ts
Audit marker comments appended to build and migration scripts.
Core Configuration & Index
src/config.ts, src/index.ts, src/plugin.ts
Marker comments added to core module entry points and configuration.
AI Services
src/services/ai/ai-provider-factory.ts, src/services/ai/opencode-provider.ts, src/services/ai/provider-config.ts, src/services/ai/providers/*, src/services/ai/session/*, src/services/ai/tools/*, src/services/ai/validators/*
Audit markers inserted across AI provider implementations, session management, tool definitions, and validators.
Core Services
src/services/api-handlers.ts, src/services/auto-capture.ts, src/services/cleanup-service.ts, src/services/client.ts, src/services/context.ts, src/services/deduplication-service.ts, src/services/embedding.ts, src/services/jsonc.ts, src/services/language-detector.ts, src/services/logger.ts, src/services/memory-*.ts, src/services/migration-service.ts, src/services/platform-server.ts, src/services/privacy.ts, src/services/retrieval-context.ts, src/services/secret-resolver.ts
Marker comments appended to service modules for memory management, logging, privacy, deduplication, and platform operations.
SQLite & Vector Backend Services
src/services/sqlite/*, src/services/vector-backends/*
Audit markers added to database connection, shard, transcript management, and vector search backend implementations.
User Profile & Web Services
src/services/user-profile/*, src/services/user-prompt/user-prompt-manager.ts, src/services/web-server.ts, src/services/web-server-worker.ts, src/web/app.js, src/web/i18n.js
Marker comments inserted into user profile context, utilities, types, and web server modules.
Type Definitions
src/types/index.ts, src/types/usearch.d.ts
Audit markers appended to TypeScript type definition files.
Test Files
tests/*.test.ts, tests/vector-backends/*.test.ts
Audit marker comments added to end of all test files; also removes stray corrupted lines (440–441) from tests/openai-chat-completion-provider.test.ts.

Estimated Code Review Effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

This is a homogeneous, mechanically repetitive change across 72 files (one identical comment line per file). Despite the large file count, the pattern is uniform and cosmetic, requiring only spot-checking to verify consistency.

Possibly Related PRs

  • ZeR020/opencode-mem0#6: Adds identical non-functional audit-marker comments to many of the same files in a parallel initiative.

Poem

🐰 A marker here, a marker there,
Across each file with hoppy care,
The audit trail now shines so bright,
Seventy-two commits done right!
One little comment, oh so small,
Labels the codebase, marking all.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '🔍 Full Codebase Audit — DO NOT MERGE' clearly describes the primary purpose of this PR: an audit-focused code review of the entire codebase with an explicit instruction not to merge.
Description check ✅ Passed The PR description is comprehensive and well-structured, providing clear context about the audit scope (83 files), specific review focus areas (8 categories), and runtime environment. While it doesn't follow the provided template structure exactly, it provides substantial and relevant information for reviewers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit-full-repo-2

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@ZeR020 Absolutely! I'll kick off a comprehensive full-repo audit across all 83 files, covering all the areas you've specified:

  1. 🔒 Security — injection risks, path traversal, data exposure
  2. 🏷️ Type Safetyany casts, missing null checks, unsafe assertions
  3. 🛡️ Error Handling — unhandled rejections, missing try/catch, silent failures
  4. 💧 Resource Leaks — unclosed DB connections, file handles, event listeners
  5. 🐛 Logic Bugs — race conditions, off-by-one, async/await issues
  6. 🪟 Cross-Platform — Windows path handling, shell command safety
  7. 🧪 Tests — coverage gaps, flaky tests, mock leaks
  8. Performance — N+1 queries, memory leaks, inefficient algorithms

The full audit will cover all 56 source files, 22 test files, and 3 scripts across your Bun 1.x / Node.js 20+ runtime targeting Windows, Linux, and macOS. I'll post detailed findings inline on the relevant files shortly.

[full_review]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (33)
src/services/web-server.ts (2)

155-165: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset startPromise on shutdown to allow clean restart.

Line 155-165: after stop(), startPromise remains set, so later start() can short-circuit without re-binding the server.

Suggested fix
  async stop(): Promise<void> {
    this.stopHealthCheckLoop();
+   this.startPromise = null;

    if (!this.isOwner || !this.server) {
      return;
    }

    this.server.stop();
    this.server = null;
    this.isOwner = false;
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/web-server.ts` around lines 155 - 165, stop() currently clears
server and isOwner but leaves startPromise set, causing subsequent start() calls
to short-circuit and not re-bind; update the stop() method (class method stop)
to also clear/reset this.startPromise (e.g., set to null/undefined) after
shutting down the server so that start() will create a new promise and perform a
fresh bind on restart.

449-452: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

changelogId query parameter is currently broken by a typo.

Line 450 reads chlogId while Line 452 expects changelogId, so clients using the documented name fail.

Suggested fix
- const changelogId = url.searchParams.get("chlogId");
+ const changelogId =
+   url.searchParams.get("changelogId") ?? url.searchParams.get("chlogId");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/web-server.ts` around lines 449 - 452, The query parameter key
is misspelled: the code reads url.searchParams.get("chlogId") but the rest of
the handler uses the variable changelogId and the documented param name is
"changelogId"; change the searchParams key to "changelogId" (replace "chlogId"
with "changelogId") so the const changelogId = url.searchParams.get(...) call
returns the expected value and the existing error branch remains correct.
src/services/platform-server.ts (1)

38-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a request body size cap to prevent memory-exhaustion DoS.

Line 38-43 buffers the full body with no upper bound, so oversized requests can exhaust process memory.

Suggested fix
+      const MAX_BODY_BYTES = 1_048_576; // 1 MiB
+      let totalBytes = 0;
       const chunks: Buffer[] = [];
       for await (const chunk of req) {
-        chunks.push(chunk);
+        const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
+        totalBytes += buf.length;
+        if (totalBytes > MAX_BODY_BYTES) {
+          res.statusCode = 413;
+          res.end("Payload too large");
+          return;
+        }
+        chunks.push(buf);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/platform-server.ts` around lines 38 - 43, The code currently
streams the request into an unbounded Buffer array (chunks) and concatenates to
body, which allows memory-exhaustion attacks; add a MAX_BODY_SIZE constant
(e.g., bytes) and while iterating the for-await-of over req in
platform-server.ts track the accumulated size, reject the request (throw an
error or respond with 413 Payload Too Large and destroy the socket) as soon as
the size exceeds MAX_BODY_SIZE, and only call Buffer.concat(chunks) when under
the limit; update any callers of body to handle the error/413 response.
src/services/ai/providers/anthropic-messages.ts (1)

173-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging raw tool payloads in validation failures.

Line 182 logs rawData from tool output; this can leak user/profile content into logs.

Suggested fix
             log("Anthropic tool response validation failed", {
               error: String(validationError),
               stack: errorStack,
               errorType:
                 validationError instanceof Error
                   ? validationError.constructor.name
                   : typeof validationError,
               toolName: toolSchema.function.name,
               iteration: iterations,
-              rawData: JSON.stringify(toolUse).slice(0, 500),
+              rawDataPreview: "[REDACTED]",
             });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/providers/anthropic-messages.ts` around lines 173 - 183, The
current log in the Anthropic tool validation path writes the raw tool payload
(toolUse) which can leak sensitive user/profile data; update the logging inside
the validation failure block (where log(...) is called in anthropic-messages.ts)
to remove rawData logging and instead log a sanitized summary: include
toolSchema.function.name, iteration count (iterations), error/stack, and a safe,
non-sensitive indicator such as payload size or a redacted/hashed fingerprint of
toolUse (do not serialize or include user content). Ensure you reference the
existing variables error/validationError, errorStack, toolSchema.function.name
and iterations and replace JSON.stringify(toolUse) with the safe
summary/redaction before calling log.
src/services/api-handlers.ts (2)

411-443: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Delete-before-insert update flow can permanently lose memory data.

Line 411 deletes the existing record before external embedding calls (Line 417/420) and reinsertion. Any failure in between leaves the memory removed.

Please make this atomic (transactional swap or update-in-place) and only remove old state after new vectors are safely persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/api-handlers.ts` around lines 411 - 443, The current flow
deletes the vector with vectorSearch.deleteVector(...) before computing
embeddings and reinserting, so failures (embeddingService.embedWithTimeout or
insertVector) can permanently lose data; change to compute new embeddings first
(call embeddingService.embedWithTimeout on newContent and tags), then insert the
new vector (vectorSearch.insertVector) into the same shard/db (or use an
update-in-place API if available), and only upon successful insert delete the
old vector and call shardManager.decrementVectorCount(foundShard.id) /
incrementVectorCount(foundShard.id) as appropriate; ensure operations use the
same foundShard and db and handle errors by rolling back the new insert if
delete fails or by avoiding delete altogether via an atomic update method.

1091-1097: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

resolved filter is ignored in conflict listing.

Line 1092 accepts resolved, but Line 1096 always returns unresolved conflicts, so API behavior is misleading.

Either apply the resolved flag in data retrieval or reject unsupported values explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/api-handlers.ts` around lines 1091 - 1097, handleListConflicts
currently ignores the resolved parameter by always calling
getAllUnresolvedConflicts; update the handler to honor the resolved flag (either
call an existing getAllConflicts/getConflicts API with a resolved parameter or
extend getAllUnresolvedConflicts to accept a resolved boolean) and map the
returned list the same way, or explicitly validate and reject unsupported
resolved values (throw or return 400) if you intend not to support filtering;
ensure changes reference handleListConflicts and getAllUnresolvedConflicts (or
the replacement conflict-retrieval function) so the correct data path is used.
src/services/cleanup-service.ts (2)

65-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

linkedMemoriesDeleted is never updated.

linkedMemoriesDeleted (Line 65) is returned (Line 113) but never incremented, so the reported value is always 0.

Also applies to: 113-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/cleanup-service.ts` around lines 65 - 66, linkedMemoriesDeleted
is never incremented so the returned count is always 0; update the deletion
logic inside the cleanup routine (where you iterate over memories and call
delete/remove) to increment linkedMemoriesDeleted whenever a linked memory is
actually deleted, and likewise ensure pinnedSkipped is incremented in the branch
that skips pinned memories; look for symbols linkedMemoriesDeleted and
pinnedSkipped in cleanup-service.ts and add the increments in the corresponding
delete/skip branches before returning those variables.

40-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update cleanup cooldown only after successful completion.

At Line 41, lastCleanupTime is set before the cleanup finishes. A failure still blocks retries for a full day.

Suggested fix
-    this.isRunning = true;
-    this.lastCleanupTime = Date.now();
+    this.isRunning = true;

     try {
@@
-      return {
+      const result = {
         deletedCount: totalDeleted,
         userCount: userDeleted,
         projectCount: projectDeleted,
         promptsDeleted,
         linkedMemoriesDeleted,
         pinnedMemoriesSkipped: pinnedSkipped,
       };
+      this.lastCleanupTime = Date.now();
+      return result;
     } finally {
       this.isRunning = false;
     }

Also applies to: 116-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/cleanup-service.ts` around lines 40 - 42, The code sets
this.lastCleanupTime before the cleanup completes, blocking retries on failure;
change the logic in the cleanup routine(s) so that this.lastCleanupTime =
Date.now() is only assigned after successful completion of the cleanup (e.g.,
inside the successful branch of the try block or immediately after await
performCleanup()), not at the start; keep this.isRunning toggles as-is but
ensure isRunning is reset in a finally block; apply the same change for the
second instance around lines where this.isRunning/this.lastCleanupTime appear
(the other cleanup invocation).
scripts/migrate-v1-to-v2.ts (4)

306-344: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Database handles are not reliably closed on per-DB exceptions.

If an error occurs after new Database(dbPath), the catch path logs but does not guarantee db.close(), risking handle leaks/locks.

Suggested fix
   for (const dbPath of dbs) {
+    let db: DatabaseType | null = null;
     try {
-      const db = new Database(dbPath);
+      db = new Database(dbPath);
@@
-      db.run("PRAGMA wal_checkpoint(TRUNCATE)");
-      db.close();
+      db.run("PRAGMA wal_checkpoint(TRUNCATE)");
     } catch (error) {
       const msg = String(error);
       result.errors.push(`${dbPath}: ${msg}`);
       log("Migration: database error", { dbPath, error: msg });
+    } finally {
+      try {
+        db?.close();
+      } catch {}
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-v1-to-v2.ts` around lines 306 - 344, The per-database try
block can throw after new Database(dbPath) and currently never guarantees
db.close(); declare a Database variable (e.g., let db: Database | null = null)
before the try, assign it when constructing with new Database(dbPath), and move
the per-database logic (isV1Schema, addV2Columns, backfillScores,
createConflictsTable, addV2Indexes, PRAGMA wal_checkpoint) inside the try; then
add a finally block that checks if db is non-null and calls db.close() to ensure
the handle is always closed even on exceptions; keep the existing error capture
in the catch but do not rely on it for closing the connection.

354-354: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace process.env.HOME with os.homedir() for Windows compatibility.

Line 354 relies on process.env.HOME, which is unset on Windows and falls back to ".", breaking the intended home directory fallback logic. Node.js recommends os.homedir() for cross-platform home directory resolution—it uses USERPROFILE on Windows and $HOME on POSIX systems.

Suggested fix
+import { homedir } from "node:os";
@@
-const storagePath = process.argv[2] || join(process.env.HOME || ".", ".opencode-mem", "data");
+const storagePath = process.argv[2] || join(homedir(), ".opencode-mem", "data");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-v1-to-v2.ts` at line 354, The storagePath initialization uses
process.env.HOME which is not set on Windows; replace that usage with
os.homedir() (importing Node's os module if not already) so the line that
defines storagePath (the const storagePath = ... statement) falls back to the
real user home directory cross-platform; update the import/require at the top to
include os and use os.homedir() in place of process.env.HOME.

175-176: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

FTS type mismatch: TEXT values inserted into INTEGER rowid field.

content_rowid='id' references transcripts.id TEXT PRIMARY KEY, but the triggers directly insert TEXT values (new.id, old.id) into FTS's rowid field, which expects INTEGER. This type inconsistency can cause FTS lookup failures and sync issues during inserts and updates.

Fix: Either change id to INTEGER PRIMARY KEY, or refactor triggers to use SQLite's implicit rowid instead of the TEXT column directly.

Affects: All FTS triggers for transcripts_fts (insert at 198-201, delete at 190-195, update at 214-216).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-v1-to-v2.ts` around lines 175 - 176, The FTS triggers for
transcripts_fts reference content_rowid='id' but the transcripts.id column is
TEXT, causing TEXT values to be inserted into the FTS INTEGER rowid; fix by
either making transcripts.id an INTEGER PRIMARY KEY or (preferable) change the
FTS wiring to use the implicit rowid: update the CREATE VIRTUAL TABLE /
content_rowid config to use the rowid (or remove content_rowid so it defaults),
and modify the triggers that currently insert new.id / old.id to insert
new.rowid / old.rowid instead (adjust the INSERT, DELETE and UPDATE triggers
that reference new.id/old.id accordingly).

1-4: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Relative imports are incorrect for a file under scripts/.

From scripts/migrate-v1-to-v2.ts, ./src/... resolves to scripts/src/..., which does not exist. Additionally, the imports reference .js files, but the actual source files are TypeScript (.ts).

Change ./src/ to ../src/ and update the extensions from .js to .ts:

Suggested fix
-import { getDatabase } from "./src/services/sqlite/sqlite-bootstrap.js";
+import { getDatabase } from "../src/services/sqlite/sqlite-bootstrap.ts";
@@
-import { log } from "./src/services/logger.js";
+import { log } from "../src/services/logger.ts";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-v1-to-v2.ts` around lines 1 - 4, The imports at the top
(getDatabase, log and the node fs/path imports) use "./src/..." and ".js"
extensions which resolve incorrectly from scripts/; update those module
specifiers to use "../src/..." and change the TypeScript file extensions from
.js to .ts (e.g., import { getDatabase } from
"../src/services/sqlite/sqlite-bootstrap.ts" and import { log } from
"../src/services/logger.ts") so getDatabase and log resolve to the actual TS
sources while keeping the built-in node imports unchanged.
src/services/ai/providers/openai-responses.ts (1)

149-149: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use resolved timeout value in the abort error message.

Line 149 references this.config.iterationTimeout; when unset, message becomes undefinedms despite using fallback iterationTimeout.

Suggested fix
-            error: `API request timeout (${this.config.iterationTimeout}ms)`,
+            error: `API request timeout (${iterationTimeout}ms)`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/providers/openai-responses.ts` at line 149, The abort error
message is using this.config.iterationTimeout which can be undefined; update the
error text to use the resolved timeout variable used earlier in the function
(e.g., iterationTimeout or resolvedIterationTimeout) so the message shows the
actual millisecond value; locate the abort creation in OpenAIResponses
(openai-responses.ts) and replace this.config.iterationTimeout with the local
resolved timeout variable used when building the AbortController.
src/services/retrieval-context.ts (1)

159-162: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against invalid threshold values in penalty calculation.

When threshold >= 1, Line 161 can divide by zero or produce invalid values.

Suggested fix
-  if (maxSimilarity > threshold) {
+  if (threshold >= 1) return 0;
+  if (maxSimilarity > threshold) {
     // Apply penalty proportional to how much over threshold
     return (maxSimilarity - threshold) / (1 - threshold);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/retrieval-context.ts` around lines 159 - 162, The penalty
calculation can divide by zero when threshold >= 1; update the function in
retrieval-context.ts that computes the penalty (the block using maxSimilarity
and threshold) to early-return a safe value (e.g., 0) when threshold >= 1 (or
clamp threshold to a valid <1 range) before performing (maxSimilarity -
threshold) / (1 - threshold), ensuring you still handle normal cases where
threshold < 1.
src/services/tags.ts (1)

131-136: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Windows path handling in getProjectName() fails after normalization.

At Line 134, normalize() converts forward slashes to backslashes on Windows. Splitting only on / at Line 135 leaves the full path intact instead of extracting the basename.

Suggested fix
 export function getProjectName(directory: string): string {
-  // Normalize path to handle both Unix and Windows separators.
-  // path.normalize on Linux does not convert backslashes, so we
-  // manually replace them before normalizing.
-  const normalized = normalize(directory.replace(/\\/g, "/"));
-  const parts = normalized.split("/").filter((p) => p);
+  const parts = directory.split(/[\\/]+/).filter((p) => p);
   return parts[parts.length - 1] || directory;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/tags.ts` around lines 131 - 136, getProjectName() currently
normalizes the input path into the variable normalized then splits on "/" which
fails on Windows because normalize() may produce backslashes; update the logic
so after computing normalized you extract the basename using Node's path
utilities (e.g., path.basename) or split on both separators (use path.sep or a
regex like /[\\/]/) to return the last path segment; modify the code referencing
normalized and parts to use this cross-platform approach so the return value is
the correct project name on all OSes.
src/services/ai/validators/user-profile-validator.ts (1)

30-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use presence checks instead of truthiness for optional nested section validation.

Lines 30/34/38 currently skip validation when properties contain falsy invalid values (e.g. empty string, 0, false), allowing malformed payloads to pass as valid. For example, { preferences: "" } bypasses validation and incorrectly returns valid, when preferences must be an array.

Suggested fix
-    if (data.preferences) {
+    if (Object.prototype.hasOwnProperty.call(data, "preferences")) {
       const prefErrors = this.validatePreferences(data.preferences);
       errors.push(...prefErrors);
     }
-    if (data.patterns) {
+    if (Object.prototype.hasOwnProperty.call(data, "patterns")) {
       const patternErrors = this.validatePatterns(data.patterns);
       errors.push(...patternErrors);
     }
-    if (data.workflows) {
+    if (Object.prototype.hasOwnProperty.call(data, "workflows")) {
       const workflowErrors = this.validateWorkflows(data.workflows);
       errors.push(...workflowErrors);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/validators/user-profile-validator.ts` around lines 30 - 40,
The current guards use truthiness and skip validation for falsy-but-present
values (e.g. empty string), so update the conditional checks around
data.preferences, data.patterns, and data.workflows to test presence explicitly
(e.g. check for !== undefined && !== null or use the 'in' operator) before
calling validatePreferences, validatePatterns, and validateWorkflows; this
ensures payloads like { preferences: "" } still run through the validator
functions and return appropriate errors.
src/services/logger.ts (1)

60-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make log() non-throwing and redact sensitive fields.

Line 61 calls JSON.stringify() without error handling. If data contains circular references, this will throw and break caller execution. Additionally, raw logging of unknown objects exposes sensitive values like tokens, passwords, and API keys without redaction.

Suggested fix
+function safeStringify(data: unknown): string {
+  try {
+    return JSON.stringify(data, (key, value) => {
+      if (/token|secret|password|api[-_]?key|authorization/i.test(key)) {
+        return "[REDACTED]";
+      }
+      return value;
+    });
+  } catch {
+    return '"[Unserializable data]"';
+  }
+}
+
 export function log(message: string, data?: unknown) {
   ensureLoggerInitialized();
   const logFile = getLogFilePath();
   const timestamp = new Date().toISOString();
   const line = data
-    ? `[${timestamp}] ${message}: ${JSON.stringify(data)}\n`
+    ? `[${timestamp}] ${message}: ${safeStringify(data)}\n`
     : `[${timestamp}] ${message}\n`;
   appendFileSync(logFile, line);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/logger.ts` around lines 60 - 63, The log() implementation
currently calls JSON.stringify(data) and then appendFileSync(logFile) which can
throw on circular structures and also writes raw sensitive fields; change log()
to be non-throwing by wrapping serialization and file append in try/catch and
swallowing/logging errors internally, and implement a safe serializer used
before appendFileSync: recursively redact well-known sensitive keys (e.g.,
"password", "token", "apiKey", "secret", "authorization", "cookie",
"accessToken") from the object returned by the serializer, then attempt
JSON.stringify; if stringify still fails, fall back to a safe textual
representation (e.g., util.inspect or "[Unserializable data]") so appendFileSync
always receives a string and never throws to callers, keeping references to
log(), appendFileSync, and logFile to locate the change.
tests/vector-backends/exact-scan-backend.test.ts (1)

13-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close SQLite connections before temp directory cleanup.

Lines 43 and 78 create DB instances via new Database(), but the afterEach hook (lines 13-18) only removes directories without closing them. Unclosed database handles can hold file locks, causing intermittent cleanup failures—particularly on Windows—since rmSync cannot remove directories with locked files.

Both BunSqliteDatabase and BetterSqlite3Database implement the close() method (sqlite-bootstrap.ts lines 67-69, 112-114), so database cleanup is supported.

Suggested fix
 describe("ExactScanBackend", () => {
   const tempDirs: string[] = [];
+  const dbs: Array<{ close?: () => void }> = [];

   afterEach(() => {
+    while (dbs.length > 0) {
+      const db = dbs.pop();
+      try {
+        db?.close?.();
+      } catch {}
+    }
     while (tempDirs.length > 0) {
       const dir = tempDirs.pop();
       if (dir) rmSync(dir, { recursive: true, force: true });
     }
   });
@@
     const db = new Database(join(tempDir, "test.db"));
+    dbs.push(db);
@@
     const db = new Database(join(tempDir, "test.db"));
+    dbs.push(db);

Also applies to: 40-43, 75-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/vector-backends/exact-scan-backend.test.ts` around lines 13 - 18, The
test cleanup currently only removes tempDirs and can fail when SQLite files are
still open; update the tests to close any database instances before deleting
directories by tracking created DB objects (those constructed with new
Database() / instances of BunSqliteDatabase or BetterSqlite3Database) and
calling their close() methods in afterEach prior to rmSync; specifically, add a
collection (e.g., dbInstances) that test setup pushes each DB into and modify
the afterEach to iterate dbInstances calling close() (await if needed) and then
clear dbInstances before removing tempDirs to ensure file handles are released.
src/services/secret-resolver.ts (1)

39-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Parse file:// values with URL utilities instead of string slicing.

Manual slicing at line 40 fails to decode URL-encoded characters and handle platform-specific paths. For example, file:///path/my%20file.txt will attempt to read a file literally named my%20file.txt instead of my file.txt. Use fileURLToPath() from the Node.js standard library for correct cross-platform behavior.

Suggested fix
 import { existsSync, readFileSync, statSync } from "node:fs";
 import { join } from "node:path";
 import { homedir, platform } from "node:os";
+import { fileURLToPath } from "node:url";
@@
   if (value.startsWith("file://")) {
-    const filePath = expandPath(value.slice(7));
+    const filePath = expandPath(fileURLToPath(value));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/secret-resolver.ts` around lines 39 - 41, The code is slicing
"file://" strings (value.slice(7)) which doesn't decode URL-encoded characters
or handle platform file URLs; replace that logic by using Node's
fileURLToPath(new URL(value)) to convert the file:// URL to a proper filesystem
path, then pass that result to expandPath (instead of the sliced string). Import
fileURLToPath (and URL from 'url' if needed) in secret-resolver.ts, use
fileURLToPath(new URL(value)) to produce filePath, and keep the rest of the
logic (reading/using filePath) unchanged.
src/services/embedding.ts (1)

24-28: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Timeout wrapper should cancel in-flight operations, not only race them.

Current timeout logic rejects the caller but leaves the underlying fetch and model work running. This can accumulate dangling requests and processing threads under load.

The remote API path (line 89) lacks an signal parameter in fetch, and the local model path (line 108) provides no cancellation mechanism. The fix should follow the AbortController pattern already established in other providers (e.g., anthropic-messages.ts), where the signal is passed to fetch and a timer cleanup is guaranteed via finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/embedding.ts` around lines 24 - 28, The withTimeout function
currently races the promise but doesn't cancel in-flight work; change it to
create an AbortController and a timeout that calls controller.abort(), pass
controller.signal into the remote fetch call (the fetch invocation in the remote
API path) and into any local model request that supports cancellation (the local
model invocation in the local model path), and ensure the timeout timer is
cleared in a finally block so you don't leak timers; keep the exported function
name withTimeout and ensure callers use the returned controller.signal for fetch
and model calls, or modify withTimeout to accept a callback that receives the
signal and returns the cancellable promise.
src/services/sqlite/vector-search.ts (1)

560-567: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape LIKE wildcards in sessionID before metadata search.

sessionID is interpolated into a LIKE pattern, so %/_ can broaden matches and return unrelated records.

🔒 Suggested fix
-    const stmt = db.prepare(`
+    const stmt = db.prepare(`
       SELECT * FROM memories
-      WHERE metadata LIKE ? AND is_deprecated = 0
+      WHERE metadata LIKE ? ESCAPE '\\' AND is_deprecated = 0
       ORDER BY created_at DESC
     `);
-
-    const rows = stmt.all(`%"sessionID":"${sessionID}"%`) as any[];
+    const escapedSessionID = sessionID.replace(/[\\%_]/g, "\\$&");
+    const rows = stmt.all(`%"sessionID":"${escapedSessionID}"%`) as any[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/sqlite/vector-search.ts` around lines 560 - 567, The metadata
search builds a LIKE pattern with unescaped sessionID which allows %/_ in
sessionID to act as wildcards; update the code that prepares and executes the
query (the db.prepare(...) call that populates stmt and the stmt.all(...) call
that computes rows) to escape SQL LIKE wildcards in sessionID (replace % and _
and escape the escape char) and pass the safe pattern as a parameter (e.g.,
'%escapedSessionID%') and/or add an ESCAPE clause so the metadata LIKE ? only
matches the literal sessionID substring.
src/services/ai/opencode-provider.ts (1)

49-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace empty catch {} blocks with explicit handling/logging.

Swallowing errors in auth IO and request mutation paths can silently continue with stale credentials or malformed requests, making outages hard to diagnose.

💡 Minimal hardening example
-    } catch {}
+    } catch (error) {
+      throw new Error(`Failed to read auth file '${authPath}': ${String(error)}`);
+    }

...
-        } catch {}
+        } catch (error) {
+          throw new Error(`Failed to persist refreshed OAuth token: ${String(error)}`);
+        }

Also applies to: 115-123, 163-185, 190-202

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/opencode-provider.ts` around lines 49 - 52, The empty catch
blocks around file I/O and request mutation in opencode-provider.ts (e.g., the
readFileSync(authPath, "utf-8") call and the other try/catch regions at lines
~115-123, ~163-185, ~190-202) must be replaced with explicit error handling: log
the caught Error via the existing logger (or throw if appropriate) and add a
clear context message (e.g., "failed reading auth file at authPath" or "error
mutating request in <functionName>") so failures won’t be silently swallowed;
locate the try/catch wrappers around readFileSync and the request mutation
functions (names referenced in the diff) and ensure each catch either
processLogger.error(err, ...) or rethrows after logging, and avoid leaving empty
catches.
src/services/ai/session/ai-session-manager.ts (1)

23-31: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Avoid import-time crash from eager AISessionManager initialization.

new AISessionManager() runs at module load, and constructor dereferences CONFIG.storagePath immediately. CI already shows this can throw before tests/runtime init complete.

💡 Suggested fix direction (lazy singleton)
-export const aiSessionManager = new AISessionManager();
+let _aiSessionManager: AISessionManager | null = null;
+
+export function getAISessionManager(): AISessionManager {
+  if (_aiSessionManager) return _aiSessionManager;
+  _aiSessionManager = new AISessionManager();
+  return _aiSessionManager;
+}

Also applies to: 225-225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/session/ai-session-manager.ts` around lines 23 - 31, The
AISessionManager is being eagerly instantiated at module load and its
constructor immediately dereferences CONFIG.storagePath (dbPath,
connectionManager.getConnection, sessionRetentionMs, initDatabase), which can
crash during import; change to a lazy singleton: remove the top-level new
AISessionManager() and export a getAISessionManager() (or
AISessionManager.getInstance()) that creates the instance on first call, and
modify the constructor (or shift work into an async init method like
initDatabase or initialize) so it does not access CONFIG or perform file IO
during class construction but instead reads CONFIG.storagePath and opens the DB
only when the singleton is first requested; update any call sites that used the
exported instance (including the other occurrence referenced) to call the lazy
getter and await initialization as needed.
tests/tool-scope.test.ts (1)

9-39: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Mock AISessionManager to prevent constructor execution during module load.

AISessionManager is exported as a singleton that instantiates immediately when imported (line 225 of ai-session-manager.ts). Its constructor accesses CONFIG.storagePath and CONFIG.aiSessionRetentionDays at lines 24 and 30. When the test imports src/index.js, the import chain triggers AIProviderFactoryai-session-managernew AISessionManager(), which attempts to access CONFIG before mockConfig is assigned. Since mockConfig is assigned after the vi.mock() calls (lines 41-96), the CONFIG mock receives undefined.

Add this mock before the CONFIG mock:

🔧 Proposed fix
 vi.mock("../src/services/language-detector.js", () => ({ getLanguageName: () => "English" }));
+vi.mock("../src/services/ai/session/ai-session-manager.js", () => ({
+  aiSessionManager: {
+    createSession: vi.fn(),
+    getSession: vi.fn(),
+    addMessage: vi.fn(),
+    getMessages: vi.fn(),
+    getLastSequence: vi.fn(() => 0),
+    updateSession: vi.fn(),
+    cleanupExpiredSessions: vi.fn(() => 0),
+  },
+}));

 vi.mock("../src/config.js", () => ({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/tool-scope.test.ts` around lines 9 - 39, AISessionManager is being
instantiated during module import and reads CONFIG before mockConfig is set; to
fix, add a vi.mock for the ai-session-manager module before the CONFIG mock that
replaces the real exports with a stubbed AISessionManager class (no-op
constructor) and a dummy exported singleton (or named export used by imports) so
the real constructor never runs during test load; reference AISessionManager,
ai-session-manager (module), CONFIG, mockConfig and the import chain via
AIProviderFactory/src/index.js to locate where to insert this new mock (place it
above the existing CONFIG mock).
tests/language-detector.test.ts (1)

9-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate test case.

This test is identical to the test on lines 5-7 (same input, same expected output). The description mentions "short Chinese (5+ chars)" but uses an 8-character string identical to the first test. Consider either removing the duplicate or testing with exactly 5 characters to verify the minLength: 5 boundary.

💚 Proposed fix to test the actual boundary
   it("should detect short Chinese as zh (5+ chars)", () => {
-    expect(detectLanguage("这是一个中文测试")).toBe("zh");
+    expect(detectLanguage("这是一个中")).toBe("zh"); // exactly 5 chars
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/language-detector.test.ts` around lines 9 - 11, The test duplicates an
existing case for detectLanguage using the same 8-character Chinese string;
update the test in tests/language-detector.test.ts to either remove this
duplicate or change the input to a true 5-character Chinese string to exercise
the minLength boundary (keep the test name "should detect short Chinese as zh
(5+ chars)" and assert detectLanguage(...) === "zh"); ensure the unique symbol
detectLanguage is referenced and the test description matches the new input.
src/services/privacy.ts (1)

1-3: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Edge case: nested <private> tags may leak content.

The regex /<private>[\s\S]*?<\/private>/gi uses non-greedy matching, which handles multiple separate <private> blocks correctly. However, nested tags like <private>outer<private>inner</private>outer</private> would result in [REDACTED]outer</private> — leaving content exposed.

If nested tags are possible in your data, consider either:

  1. Documenting that nesting is unsupported, or
  2. Iteratively applying the regex until no matches remain.
🛡️ Iterative approach if nesting is possible
 export function stripPrivateContent(content: string): string {
-  return content.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]");
+  let result = content;
+  let prev: string;
+  do {
+    prev = result;
+    result = result.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]");
+  } while (result !== prev);
+  return result;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/privacy.ts` around lines 1 - 3, The current stripPrivateContent
function uses the regex /<private>[\s\S]*?<\/private>/gi which fails on nested
<private>…</private> tags and can leak inner content; modify stripPrivateContent
to repeatedly apply the regex (loop calling content.replace(...) until no
replacements occur) or implement a simple stack-based parser to fully remove
nested blocks, ensuring the function returns fully redacted text for inputs like
"<private>outer<private>inner</private>outer</private>".
src/services/ai/providers/google-gemini.ts (2)

94-94: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Uncaught JSON.parse may throw on malformed tool call arguments.

JSON.parse(tc.function.arguments) at line 94 and JSON.parse(msg.content) at line 107 are not wrapped in try/catch. Malformed JSON from the API or stored messages will cause an unhandled exception, breaking the entire tool call flow.

🐛 Proposed fix: wrap in try/catch
           if (msg.toolCalls) {
             for (const tc of msg.toolCalls) {
+              try {
                 parts.push({
                   functionCall: {
                     name: tc.function.name,
                     args: JSON.parse(tc.function.arguments),
                   },
                 });
+              } catch {
+                log("Gemini: failed to parse tool call arguments", { toolCallId: tc.id });
+              }
             }
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/providers/google-gemini.ts` at line 94, Wrap the unguarded
JSON.parse calls for tc.function.arguments and msg.content in try/catch inside
the Google Gemini tool-call handling code so malformed JSON doesn't throw; on
parse failure log the error via the existing logger (or processLogger), set a
safe fallback (e.g., {} for args or an empty string), and propagate a controlled
error or mark the tool call as failed instead of letting an exception bubble.
Locate the parsing sites that reference tc.function.arguments and msg.content
and replace direct JSON.parse(...) with a guarded parse function or inline
try/catch that returns a safe default and records the parse error.

154-154: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

API key in URL query string may leak in logs or error messages.

The API key is passed as a URL query parameter. While this is Google's standard approach, the URL may be logged in error scenarios (line 182-188 logs the error which could include the URL in stack traces). Consider redacting the key from any logged URLs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/ai/providers/google-gemini.ts` at line 154, The URL currently
includes the API key in the query string via const url =
`${baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
to avoid leaking the key in logs, stop logging the raw url variable and instead
create a redactedUrl (or build urlWithoutKey =
`${baseUrl}/models/${this.config.model}:generateContent`) for any log messages;
keep using the original url (or pass apiKey separately) when making the HTTP
request but ensure the error logging block that references url (the
catch/logging code in this file) logs redactedUrl or omits this.config.apiKey
altogether; alternatively, if the API supports it, move this.config.apiKey into
an Authorization header and construct the request accordingly, but in either
case ensure no logs include the plaintext API key.
src/services/sqlite/transcript-manager.ts (1)

74-80: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

FTS5 configuration error breaks transcript saving: id is TEXT but content_rowid expects INTEGER.

The FTS5 virtual table declares content_rowid='id', but the transcripts table defines id as TEXT PRIMARY KEY. This causes the triggers (lines 83-107) to fail when inserting records—they attempt to insert TEXT values into the integer rowid column, resulting in "datatype mismatch" errors. This breaks the saveTranscript() method entirely.

The searchTranscripts() method (line 193) also has a malformed JOIN on fts.rowid = t.id (integer vs. text comparison), though this function is unused.

🐛 Suggested fix: use implicit rowid
     // FTS5 virtual table for full-text search on messages
     db.run(`
       CREATE VIRTUAL TABLE IF NOT EXISTS transcripts_fts USING fts5(
         messages,
-        content='transcripts',
-        content_rowid='id'
+        content='transcripts'
       )
     `);

Then update triggers to use rowid instead of id:

     CREATE TRIGGER IF NOT EXISTS transcripts_fts_insert 
     AFTER INSERT ON transcripts BEGIN
-      INSERT INTO transcripts_fts(rowid, messages) 
-      VALUES (new.id, new.messages);
+      INSERT INTO transcripts_fts(rowid, messages) 
+      VALUES (new.rowid, new.messages);
       END

And fix the search query:

-        WHERE transcripts_fts MATCH ?
+        WHERE t.rowid IN (SELECT rowid FROM transcripts_fts WHERE transcripts_fts MATCH ?)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/sqlite/transcript-manager.ts` around lines 74 - 80, The FTS5
virtual table uses content_rowid='id' while the transcripts table defines id as
TEXT, causing datatype mismatches and failing the triggers and saveTranscript();
change the FTS5 setup to use the implicit integer rowid (remove
content_rowid='id' and don’t map it to the TEXT id), update the associated
triggers (the INSERT/UPDATE/DELETE triggers that reference transcripts_fts and
the transcripts table) to use rowid instead of id when inserting/updating the
FTS table, and fix searchTranscripts() to join on fts.rowid = t.rowid (or
compare integer rowid to rowid) so the JOINs and trigger operations use the
implicit integer rowid consistently and avoid text/integer mismatches while
leaving the external TEXT id column unchanged.
src/services/memory-scoring-service.ts (1)

76-163: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing rollback on shard-level failures after BEGIN TRANSACTION

When any error occurs before COMMIT, the catch path logs and continues without ROLLBACK, which can leave an open transaction and cause lock/state issues.

Minimal fix
-        db.run("BEGIN TRANSACTION");
+        let inTxn = false;
+        db.run("BEGIN TRANSACTION");
+        inTxn = true;
 ...
-        db.run("COMMIT");
+        db.run("COMMIT");
+        inTxn = false;
         shardsProcessed++;
       } catch (error) {
+        try {
+          // best-effort cleanup when transaction was started
+          db.run?.("ROLLBACK");
+        } catch {}
         log("Score recalculation failed for shard", {
           shardId: shard.id,
           error: String(error),
         });
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/memory-scoring-service.ts` around lines 76 - 163, The shard loop
begins a transaction with db.run("BEGIN TRANSACTION") but on error the catch
path only logs and does not call ROLLBACK, leaving transactions open; update the
error handling around the shard processing (the block that uses updateStmt.run
and db.run("COMMIT")) to ensure you call db.run("ROLLBACK") when an exception
occurs (or use a try/catch/finally per shard) and also ensure any prepared
statement (updateStmt) is finalized/closed in the finally so resources are
released; specifically modify the code that contains db.run("BEGIN
TRANSACTION"), updateStmt, db.run("COMMIT"), and the catch block to run
db.run("ROLLBACK") on errors and finalize updateStmt in a finally.
src/services/migration-service.ts (1)

251-325: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Re-embed migration can permanently lose memories on partial failure

The source shard is deleted before reinsertion. If embedding/insertion fails for any memory, those records are dropped and the method still returns success: true.

Suggested direction
-        await shardManager.deleteShard(shardInfo.shardId);
-
-        for (const memory of tempMemories) {
+        let shardHadFailures = false;
+        for (const memory of tempMemories) {
           try {
             const vector = await embeddingService.embedWithTimeout(memory.content);
             ...
             await vectorSearch.insertVector(...);
             ...
           } catch (error) {
+            shardHadFailures = true;
             log("Migration: error re-embedding memory", { ... });
             processedCount++;
           }
         }
+
+        if (!shardHadFailures) {
+          await shardManager.deleteShard(shardInfo.shardId);
+        } else {
+          // keep original shard; mark migration as partial/failed
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/migration-service.ts` around lines 251 - 325, The code deletes
the source shard via shardManager.deleteShard before re-inserting memories,
which can permanently lose data on partial failure; change the flow in the
migration routine so you first re-embed and insert all tempMemories (using
embeddingService.embedWithTimeout, vectorSearch.insertVector,
vectorSearch.pinMemory, shardManager.incrementVectorCount) into the target
shards and only call shardManager.deleteShard(shardInfo.shardId) after all
inserts succeed for that shard; additionally record per-memory failures, surface
a non-success result (or include failedIds/count) in the final return instead of
always returning success: true, and ensure exception handling around
embed/insert updates processedCount but does not trigger source deletion for
partially failed shards.
src/services/auto-capture.ts (1)

30-75: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reset claimed prompts on early exit to prevent permanent deadlock

After claimPrompt() succeeds (sets captured = 2), the code has multiple early-exit paths (lines 31, 43, 50, 56, 62) and an error throw (line 35) that leave the prompt in a claimed state. Since getLastUncapturedPrompt() only retrieves prompts with captured = 0, these claimed prompts become permanently stuck and unprocessable.

Add a finally block to reset the claim if the prompt was never successfully marked as captured or deleted:

Suggested fix
let claimedPromptId: string | null = null;
try {
  // ... existing code ...
  claimedPromptId = prompt.id;
  if (!userPromptManager.claimPrompt(prompt.id)) {
    claimedPromptId = null;
    return;
  }
  // ... rest of flow ...
} finally {
  isCaptureRunning = false;
  if (claimedPromptId) {
    // Reset claim if capture didn't complete
    userPromptManager.resetPromptClaim(claimedPromptId);
  }
}

This requires implementing resetPromptClaim() to set captured = 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/auto-capture.ts` around lines 30 - 75, The prompt claiming flow
in auto-capture leaves prompts permanently stuck because claimPrompt(prompt.id)
sets captured=2 but several early returns and the thrown error never reset that
state; modify the capture routine (the async function containing the call to
userPromptManager.claimPrompt, the session fetch via
ctx.client.session.messages, extractAIContent, buildMarkdownContext, and
generateSummary) to track a claimedPromptId variable when claimPrompt succeeds,
wrap the existing logic in try/finally, and in the finally block set
isCaptureRunning = false and call a new
userPromptManager.resetPromptClaim(claimedPromptId) if claimedPromptId is set
and the prompt was not successfully deleted or marked captured; also implement
userPromptManager.resetPromptClaim(promptId) to set captured = 0 so claimed
prompts are released on all early-exit paths.
src/services/memory-scoring.ts (1)

205-210: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove g flag from shared regexes used with .test() to prevent stateful scoring bugs.

Lines 205-210 and 214-215 define global regexes that are reused in loops at lines 455 and 464 via .test(). With the g flag, lastIndex is mutated across calls, causing hasNegation and hasAction to return nondeterministic results.

Suggested fix
 const NEGATION_PATTERNS = [
-  /\b(not|no|never|none|nothing|nobody|nowhere|neither|nor)\b/gi,
-  /\b(don't|doesn't|didn't|won't|wouldn't|shouldn't|couldn't|can't|cannot)\b/gi,
-  /\b(removed|deleted|reverted|undone|cancelled|canceled|disabled|turned off)\b/gi,
-  /\b(un|dis|mis|non)[a-z]+\b/gi,
-  /\b(false|incorrect|wrong|invalid|failed|error)\b/gi,
+  /\b(not|no|never|none|nothing|nobody|nowhere|neither|nor)\b/i,
+  /\b(don't|doesn't|didn't|won't|wouldn't|shouldn't|couldn't|can't|cannot)\b/i,
+  /\b(removed|deleted|reverted|undone|cancelled|canceled|disabled|turned off)\b/i,
+  /\b(un|dis|mis|non)[a-z]+\b/i,
+  /\b(false|incorrect|wrong|invalid|failed|error)\b/i,
 ];

 const ACTION_PATTERNS = [
-  /\b(added|created|implemented|built|developed|wrote|wrote|configured|enabled|fixed|resolved|solved)\b/gi,
-  /\b(true|correct|valid|success|working|active|enabled|on)\b/gi,
+  /\b(added|created|implemented|built|developed|wrote|wrote|configured|enabled|fixed|resolved|solved)\b/i,
+  /\b(true|correct|valid|success|working|active|enabled|on)\b/i,
 ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/memory-scoring.ts` around lines 205 - 210, The shared regexes
(the arrays of patterns used for negation/action detection) are declared with
the global "g" flag and are later reused with RegExp.test() in hasNegation and
hasAction, causing stateful failures due to lastIndex; remove the "g" flag from
those regex literals (or alternatively ensure each test uses a new RegExp
instance) so that hasNegation and hasAction produce deterministic results when
calling .test() repeatedly on the same patterns.

@ZeR020
ZeR020 merged commit 2c3071e into main May 6, 2026
9 checks passed
@ZeR020
ZeR020 deleted the audit-full-repo-2 branch May 6, 2026 18:18
@ZeR020 ZeR020 changed the title 🔍 Full Codebase Audit — DO NOT MERGE fix: apply repository audit fixes May 6, 2026
@ZeR020 ZeR020 changed the title fix: apply repository audit fixes fix: apply repository quality fixes May 6, 2026
ZeR020 added a commit that referenced this pull request Jun 28, 2026
Version bump, changelog, and docs update for release.

Added:
- file://~/ path expansion in *ApiKey config fields (#34)
- initConfig() empty-config guard preserving CONFIG on transient I/O failure (#35)

Fixed:
- initConfig() silent CONFIG reset to defaults (#35, #36)
- file://~/ throws on Linux — ~ expanded before URL parsing (#34)
- Vitest critical CVE GHSA-5xrq-8626-4rwp (dev-only, updated to 3.2.6)

Removed (dead code, zero consumers):
- NSWBackend + nsw/nsw-first factory branches (production-unreachable)
- supportsSession() abstract method (always returned true)
- profile-utils.ts (zero importers)
- aiSessionManager backward-compatible Proxy export
- getSupportedProviders(), startCleanupSchedule/stopCleanupSchedule
- ConflictCheckLock class (inlined as Set)

Changed:
- iso-639-3 full dataset → Intl.DisplayNames
- Dynamic imports → static in auto-capture.ts and handlers/memory.ts
- .gitignore reorganized (280 → 73 lines)
- localStorage API key storage documented (code scanning #7)

Closed: #34, #35, #36, #37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant