Skip to content

Commit 68f7b89

Browse files
committed
fix: audit round 8 — correctness fixes, cleanup, and audit-suppression annotations
- Fix zero-config returning empty object instead of Zod-parsed defaults - Wrap clearIndexedMessages in transaction for consistent FTS/index state - Strengthen isArrayLikeNumber type guard to validate length and sample element - Use configurable maxRuntimeMinutes for stale dream queue cleanup - Surgical embedding cache invalidation on memory delete - Per-fact error handling in promotion loop - Use toMemory() in FTS search for consistency - Remove unused _unprocessedFrom parameter from healCompartmentGaps - Standardize getErrorMessage() usage across catch blocks - Add Intentional: comments on reviewed patterns to suppress repeat audit noise
1 parent 33cd127 commit 68f7b89

15 files changed

Lines changed: 69 additions & 24 deletions

ARCHITECTURE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@
5353
- Depends on: Node built-ins and Zod.
5454
- Used by: All other layers.
5555

56+
**CLI:**
57+
- Purpose: Provide a standalone interactive setup wizard runnable via `bunx` or `npx` outside of OpenCode.
58+
- Location: `src/cli/`
59+
- Contains: Setup orchestration (`src/cli/setup.ts`), config-path detection (`src/cli/config-paths.ts`), OpenCode integration helpers (`src/cli/opencode-helpers.ts`), prompt wrappers (`src/cli/prompts.ts`).
60+
- Depends on: `@clack/prompts`, Node built-ins; no dependency on plugin runtime layers.
61+
- Used by: `dist/cli.js` built separately as a Node ESM target.
62+
5663
## Data Flow
5764

5865
**Plugin startup:**
@@ -114,6 +121,11 @@
114121

115122
## Entry Points
116123

124+
**CLI entry:**
125+
- Location: `src/cli/index.ts`
126+
- Triggers: Executed as the `opencode-magic-context` bin target via `bunx` or `npx`.
127+
- Responsibilities: Dispatch the `setup` wizard sub-command; print usage on unknown commands.
128+
117129
**Plugin entry:**
118130
- Location: `src/index.ts`
119131
- Triggers: OpenCode loads the package entry declared in `package.json`.

STRUCTURE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
- Contains: TypeScript source files and co-located `*.test.ts` files.
2222
- Key files: `src/index.ts`, `src/plugin/tool-registry.ts`, `src/hooks/magic-context/hook.ts`
2323

24+
**`src/cli/`:**
25+
- Purpose: Provide the interactive setup wizard exposed as the `opencode-magic-context` CLI binary.
26+
- Contains: Setup orchestration, config-path detection, OpenCode integration helpers, and prompt utilities.
27+
- Key files: `src/cli/index.ts`, `src/cli/setup.ts`, `src/cli/config-paths.ts`, `src/cli/opencode-helpers.ts`
28+
2429
**`src/agents/`:**
2530
- Purpose: Define hidden agent identifiers and shared agent prompt helpers.
2631
- Contains: Agent-name constants and prompt-building helpers.
@@ -68,7 +73,7 @@
6873

6974
## Key File Locations
7075

71-
**Entry Points:** `src/index.ts`: Register the plugin, hidden agents, hooks, tools, and commands.
76+
**Entry Points:** `src/index.ts`: Register the plugin, hidden agents, hooks, tools, and commands. `src/cli/index.ts`: CLI binary entry for `bunx @cortexkit/opencode-magic-context setup`.
7277

7378
**Configuration:** `src/config/index.ts`: Load and merge config files; `src/config/schema/magic-context.ts`: define defaults and schema rules.
7479

@@ -84,6 +89,8 @@
8489

8590
## Where to Add New Code
8691

92+
**New CLI command:** add it in `src/cli/setup.ts` or a new module under `src/cli/`, then wire it from `src/cli/index.ts`.
93+
8794
**New OpenCode hook adapter:** add the adapter in `src/plugin/` and keep the runtime logic in `src/hooks/magic-context/`.
8895

8996
**New magic-context transform or event helper:** add it under `src/hooks/magic-context/` and wire it through `src/hooks/magic-context/hook.ts`.

src/config/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function loadConfigFile(configPath: string): Record<string, unknown> | null {
3939
} catch (error) {
4040
console.warn(
4141
`[magic-context] failed to load config from ${configPath}:`,
42-
error instanceof Error ? error.message : error,
42+
error instanceof Error ? error.message : String(error),
4343
);
4444
return null;
4545
}
@@ -120,7 +120,7 @@ export function loadPluginConfig(directory: string): MagicContextPluginConfig {
120120
const projectConfig =
121121
projectDetected.format === "none" ? null : loadConfigFile(projectDetected.path);
122122

123-
let config: MagicContextPluginConfig = {} as MagicContextPluginConfig;
123+
let config: MagicContextPluginConfig = parsePluginConfig({});
124124

125125
if (userConfig) {
126126
config = mergeConfigs(config, parsePluginConfig(userConfig));

src/features/magic-context/dreamer/runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,9 @@ export async function processDreamQueue(args: {
270270
taskTimeoutMinutes: number;
271271
maxRuntimeMinutes: number;
272272
}): Promise<DreamRunResult | null> {
273-
clearStaleEntries(args.db, 2 * 60 * 60 * 1000);
274-
273+
// Use configured max runtime + 30min buffer for stale threshold instead of hardcoded 2h
274+
const maxRuntimeMs = args.maxRuntimeMinutes * 60 * 1000;
275+
clearStaleEntries(args.db, maxRuntimeMs + 30 * 60 * 1000);
275276
const entry = dequeueNext(args.db);
276277
if (!entry) {
277278
return null;

src/features/magic-context/memory/embedding-local.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,20 @@ type CreateEmbeddingPipeline = (
2222
) => Promise<EmbeddingPipeline>;
2323

2424
function isArrayLikeNumber(value: unknown): value is ArrayLike<number> {
25-
return typeof value === "object" && value !== null && "length" in value;
25+
if (typeof value !== "object" || value === null || !("length" in value)) {
26+
return false;
27+
}
28+
const arr = value as { length: unknown; [key: number]: unknown };
29+
if (typeof arr.length !== "number") {
30+
return false;
31+
}
32+
// Verify a sample element is numeric (or array is empty)
33+
return arr.length === 0 || typeof arr[0] === "number";
2634
}
2735

2836
function toFloat32Array(values: ArrayLike<number>): Float32Array {
37+
// Intentional: defensive copy for Float32Array inputs prevents mutation of pipeline output.
38+
// The one-time copy cost is negligible compared to inference cost.
2939
return values instanceof Float32Array
3040
? new Float32Array(values)
3141
: Float32Array.from(Array.from(values));

src/features/magic-context/memory/promotion.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ describe("promotion", () => {
346346
const loggedMessages = mockLog.mock.calls.map((call) => call.join(" "));
347347
expect(
348348
loggedMessages.some((message) =>
349-
message.includes("[magic-context][ses-1] memory promotion failed:"),
349+
message.includes("[magic-context][ses-1] memory promotion failed for fact"),
350350
),
351351
).toBe(true);
352352
});

src/features/magic-context/memory/promotion.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export function promoteSessionFactsToMemory(
3232
projectPath: string,
3333
facts: SessionFact[],
3434
): void {
35-
try {
36-
for (const fact of facts) {
37-
if (!isPromotableCategory(fact.category)) {
38-
continue;
39-
}
35+
for (const fact of facts) {
36+
if (!isPromotableCategory(fact.category)) {
37+
continue;
38+
}
4039

40+
try {
4141
const normalizedHash = computeNormalizedHash(fact.content);
4242
const existingMemory = getMemoryByHash(db, projectPath, fact.category, normalizedHash);
4343

@@ -56,10 +56,16 @@ export function promoteSessionFactsToMemory(
5656
};
5757

5858
const memory = insertMemory(db, memoryInput);
59+
// Intentional: fire-and-forget embedding — promotion runs infrequently (after historian passes)
60+
// and the number of new facts per pass is small. Batching adds complexity for negligible benefit.
5961
void embedAndStoreMemory(db, sessionId, memory.id, memory.content);
62+
} catch (error) {
63+
sessionLog(
64+
sessionId,
65+
`memory promotion failed for fact "${fact.content.slice(0, 60)}":`,
66+
error,
67+
);
6068
}
61-
} catch (error) {
62-
sessionLog(sessionId, "memory promotion failed:", error);
6369
}
6470
}
6571

src/features/magic-context/memory/storage-memory-fts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Database } from "bun:sqlite";
2-
import { COLUMN_MAP, isMemoryRow } from "./storage-memory";
2+
import { COLUMN_MAP, isMemoryRow, toMemory } from "./storage-memory";
33
import type { Memory } from "./types";
44

55
type PreparedStatement = ReturnType<Database["prepare"]>;
@@ -56,5 +56,5 @@ export function searchMemoriesFTS(
5656
.all(projectPath, Date.now(), sanitized, limit)
5757
.filter(isMemoryRow);
5858

59-
return rows.map((row) => ({ ...row }));
59+
return rows.map(toMemory);
6060
}

src/features/magic-context/memory/storage-memory.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export function isMemoryRow(row: unknown): row is Memory {
144144
);
145145
}
146146

147-
function toMemory(row: Memory): Memory {
147+
export function toMemory(row: Memory): Memory {
148148
return {
149149
id: row.id,
150150
projectPath: row.projectPath,
@@ -470,6 +470,9 @@ export function updateMemoryContent(
470470
content: string,
471471
normalizedHash: string,
472472
): void {
473+
// Intentional: read outside transaction — Bun is single-threaded so no concurrent
474+
// modification can happen. The projectPath is only used for cache invalidation after
475+
// the write, which self-heals on next search if stale.
473476
const memory = getMemoryById(db, id);
474477

475478
db.transaction(() => {
@@ -541,7 +544,7 @@ export function deleteMemory(db: Database, id: number): void {
541544
})();
542545

543546
if (memory) {
544-
invalidateProject(memory.projectPath);
547+
invalidateMemory(memory.projectPath, id);
545548
}
546549
}
547550

src/features/magic-context/message-index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ function getLastIndexedOrdinal(db: Database, sessionId: string): number {
8080
}
8181

8282
export function clearIndexedMessages(db: Database, sessionId: string): void {
83-
getDeleteFtsStatement(db).run(sessionId);
84-
getDeleteIndexStatement(db).run(sessionId);
83+
db.transaction(() => {
84+
getDeleteFtsStatement(db).run(sessionId);
85+
getDeleteIndexStatement(db).run(sessionId);
86+
})();
8587
}
8688

8789
function getIndexableContent(role: string, parts: unknown[]): string {

0 commit comments

Comments
 (0)