Skip to content

Commit 251296d

Browse files
authored
Suppress informational log messages in TUI mode (#26)
## Problem All lore log messages used `console.error()` which writes to stderr. The TUI renders stderr as red error text, making routine status messages look like alarming errors to users: ![TUI showing red error-looking messages](https://i.imgur.com/placeholder.png) Examples seen in the wild: - `[lore] incremental distillation: 54 undistilled messages in ses_3502bf6e2ffe` - `[lore] pruned 9 oversized knowledge entries (confidence set to 0)` ## Solution Added `src/log.ts` with three levels: | Level | Visibility | Use case | |-------|-----------|----------| | `log.info()` | Suppressed by default | Routine operations — distillation, pruning, consolidation, imports | | `log.warn()` | Suppressed by default | Non-actionable oddities — dropping trailing messages | | `log.error()` | Always visible | Real failures — catch blocks where something actually broke | Set `LORE_DEBUG=1` to see all messages when debugging the plugin. ## Changes - **New:** `src/log.ts` — lightweight logger with LORE\_DEBUG gating - **`src/index.ts`** — 18 `console.error` calls replaced with appropriate log level - **`src/distillation.ts`** — 1 `console.error` → `log.info`
1 parent 0dba2a9 commit 251296d

4 files changed

Lines changed: 63 additions & 32 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@
2828
<!-- lore:019c91ad-4d47-7afc-90e0-239a9eda57a4 -->
2929
* **Stuck compaction loops leave orphaned user+assistant message pairs in DB**: When OpenCode compaction overflows, it creates paired user+assistant messages per retry (assistant has error.name:'ContextOverflowError', mode:'compaction'). These accumulate and worsen the session. Recovery: find last good assistant message (has tokens, no error), delete all messages after it from both \`message\` and \`part\` tables. Use json\_extract(data, '$.error.name') to identify compaction debris.
3030
<!-- lore:019c8f4f-67ca-7212-a8c4-8a75b230ceea -->
31-
* **Lore test suite uses live DB — no test isolation for db.test.ts**: The lore test suite (test/db.test.ts, test/ltm.test.ts) uses the live DB at ~/.local/share/opencode-lore/lore.db — no LORE\_DB\_PATH override. Test fixtures create entries with 019c9026-\* UUIDs that persist and leak into AGENTS.md exports. Known leaked entries: 'Kubernetes deployment pattern', 'TypeScript strict mode caveat', 'React useState async pitfall', 'Fine entry'. These require periodic manual cleanup from the DB. Fix needed: set LORE\_DB\_PATH to a temp file in tests.
31+
* **Lore test suite uses live DB — no test isolation for db.test.ts**: Lore test suite (test/db.test.ts, test/ltm.test.ts) uses the live DB at ~/.local/share/opencode-lore/lore.db — no LORE\_DB\_PATH override. Test fixtures create entries with 019c9026-\* UUIDs that persist and leak into AGENTS.md exports. Known leaked entries: 'Kubernetes deployment pattern', 'TypeScript strict mode caveat', 'React useState async pitfall', 'Fine entry'. Require periodic manual cleanup. Fix needed: LORE\_DB\_PATH temp file in tests.
3232

33-
### Preference
33+
### Pattern
3434

35-
<!-- lore:019ca19d-fc02-7657-b2e9-7764658c01a5 -->
36-
* **Code style**: User prefers no backwards-compat shims — fix callers directly. Prefer explicit error handling over silent failures. Derive thresholds from existing constants rather than hardcoding magic numbers (e.g., use \`raw.length <= COL\_COUNT\` instead of \`n < 10\_000\`). In CI, define shared env vars at workflow level, not per-job.
35+
<!-- lore:019cb050-ef48-7cbe-8e58-802f17c34591 -->
36+
* **Lore logging: LORE\_DEBUG gating for info/warn, always-on for errors**: src/log.ts provides three levels: log.info() and log.warn() are suppressed unless LORE\_DEBUG=1 or LORE\_DEBUG=true; log.error() always emits. All write to stderr with \[lore] prefix. This exists because OpenCode TUI renders all stderr as red error text — routine status messages (distillation counts, pruning stats, consolidation) were alarming users. Rule: use log.info() for successful operations and status, log.warn() for non-actionable oddities (e.g. dropping trailing messages), log.error() only in catch blocks for real failures. Never use console.error directly in plugin source files.
3737

3838
### Preference
3939

4040
<!-- lore:019ca190-0001-7000-8000-000000000001 -->
4141
* **Always dry-run before bulk DB deletes**: Never execute bulk DELETE/destructive operations without first running the equivalent SELECT to verify row count and inspect affected rows. A hardcoded timestamp off by one year caused deletion of all 1638 messages + 5927 parts instead of 5 debris rows. Pattern: (1) SELECT with same WHERE, (2) verify count, (3) then DELETE. Applies to any destructive op — DB mutations, git reset, file deletion.
42+
<!-- lore:019ca19d-fc02-7657-b2e9-7764658c01a5 -->
43+
* **Code style**: User prefers no backwards-compat shims — fix callers directly. Prefer explicit error handling over silent failures. Derive thresholds from existing constants rather than hardcoding magic numbers (e.g., use \`raw.length <= COL\_COUNT\` instead of \`n < 10\_000\`). In CI, define shared env vars at workflow level, not per-job.
4244
<!-- End lore-managed section -->

src/distillation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk";
22
import { db, ensureProject } from "./db";
33
import { config } from "./config";
44
import * as temporal from "./temporal";
5+
import * as log from "./log";
56
import {
67
DISTILLATION_SYSTEM,
78
distillationUser,
@@ -273,8 +274,8 @@ export async function run(input: {
273274
// Reset orphaned messages (marked distilled by a deleted/migrated distillation)
274275
const orphans = resetOrphans(input.projectPath, input.sessionID);
275276
if (orphans > 0) {
276-
console.error(
277-
`[lore] Reset ${orphans} orphaned messages for re-observation`,
277+
log.info(
278+
`Reset ${orphans} orphaned messages for re-observation`,
278279
);
279280
}
280281

src/index.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { formatKnowledge, formatDistillations } from "./prompt";
1919
import { createRecallTool } from "./reflect";
2020
import { shouldImport, importFromFile, exportToFile } from "./agents-file";
21+
import * as log from "./log";
2122

2223
/**
2324
* Detect whether an error from session.error is a context overflow ("prompt too long").
@@ -85,9 +86,9 @@ export const LorePlugin: Plugin = async (ctx) => {
8586
if (shouldImport({ projectPath, filePath })) {
8687
try {
8788
importFromFile({ projectPath, filePath });
88-
console.error("[lore] imported knowledge from", cfg.agentsFile.path);
89+
log.info("imported knowledge from", cfg.agentsFile.path);
8990
} catch (e) {
90-
console.error("[lore] agents-file import error:", e);
91+
log.error("agents-file import error:", e);
9192
}
9293
}
9394
}
@@ -99,7 +100,7 @@ export const LorePlugin: Plugin = async (ctx) => {
99100
if (config().knowledge.enabled) {
100101
const pruned = ltm.pruneOversized(1200);
101102
if (pruned > 0) {
102-
console.error(`[lore] pruned ${pruned} oversized knowledge entries (confidence set to 0)`);
103+
log.info(`pruned ${pruned} oversized knowledge entries (confidence set to 0)`);
103104
}
104105
}
105106

@@ -168,7 +169,7 @@ export const LorePlugin: Plugin = async (ctx) => {
168169
});
169170
}
170171
} catch (e) {
171-
console.error("[lore] distillation error:", e);
172+
log.error("distillation error:", e);
172173
} finally {
173174
distilling = false;
174175
}
@@ -185,7 +186,7 @@ export const LorePlugin: Plugin = async (ctx) => {
185186
model: cfg.model,
186187
});
187188
} catch (e) {
188-
console.error("[lore] curator error:", e);
189+
log.error("curator error:", e);
189190
}
190191
}
191192

@@ -240,8 +241,8 @@ export const LorePlugin: Plugin = async (ctx) => {
240241
) {
241242
const pending = temporal.undistilledCount(projectPath, msg.sessionID);
242243
if (pending >= config().distillation.maxSegment) {
243-
console.error(
244-
`[lore] incremental distillation: ${pending} undistilled messages in ${msg.sessionID.substring(0, 16)}`,
244+
log.info(
245+
`incremental distillation: ${pending} undistilled messages in ${msg.sessionID.substring(0, 16)}`,
245246
);
246247
backgroundDistill(msg.sessionID);
247248
}
@@ -271,11 +272,11 @@ export const LorePlugin: Plugin = async (ctx) => {
271272

272273
// Detect "prompt is too long" API errors and auto-recover.
273274
const rawError = (event.properties as Record<string, unknown>).error;
274-
console.error("[lore] session.error received:", JSON.stringify(rawError, null, 2));
275+
log.info("session.error received:", JSON.stringify(rawError, null, 2));
275276

276277
if (isContextOverflow(rawError) && errorSessionID) {
277-
console.error(
278-
`[lore] detected context overflow — auto-recovering (session: ${errorSessionID.substring(0, 16)})`,
278+
log.info(
279+
`detected context overflow — auto-recovering (session: ${errorSessionID.substring(0, 16)})`,
279280
);
280281

281282
// 1. Force layer 2 on next transform (persisted to DB — survives restarts).
@@ -294,23 +295,23 @@ export const LorePlugin: Plugin = async (ctx) => {
294295
summaries.map(s => ({ observations: s.observations, generation: s.generation })),
295296
);
296297

297-
console.error(
298-
`[lore] sending auto-recovery message to session ${errorSessionID.substring(0, 16)}`,
298+
log.info(
299+
`sending auto-recovery message to session ${errorSessionID.substring(0, 16)}`,
299300
);
300301
await ctx.client.session.prompt({
301302
path: { id: errorSessionID },
302303
body: {
303304
parts: [{ type: "text", text: recoveryText, synthetic: true }],
304305
},
305306
});
306-
console.error(
307-
`[lore] auto-recovery message sent successfully`,
307+
log.info(
308+
`auto-recovery message sent successfully`,
308309
);
309310
} catch (recoveryError) {
310311
// Recovery is best-effort — don't let it crash the event handler.
311312
// The persisted forceMinLayer will still help on the user's next message.
312-
console.error(
313-
`[lore] auto-recovery failed (forceMinLayer still persisted):`,
313+
log.error(
314+
`auto-recovery failed (forceMinLayer still persisted):`,
314315
recoveryError,
315316
);
316317
}
@@ -343,8 +344,8 @@ export const LorePlugin: Plugin = async (ctx) => {
343344
if (cfg.knowledge.enabled) try {
344345
const allEntries = ltm.forProject(projectPath);
345346
if (allEntries.length > cfg.curator.maxEntries) {
346-
console.error(
347-
`[lore] entry count ${allEntries.length} exceeds maxEntries ${cfg.curator.maxEntries} — running consolidation`,
347+
log.info(
348+
`entry count ${allEntries.length} exceeds maxEntries ${cfg.curator.maxEntries} — running consolidation`,
348349
);
349350
const { updated, deleted } = await curator.consolidate({
350351
client: ctx.client,
@@ -353,11 +354,11 @@ export const LorePlugin: Plugin = async (ctx) => {
353354
model: cfg.model,
354355
});
355356
if (updated > 0 || deleted > 0) {
356-
console.error(`[lore] consolidation: ${updated} updated, ${deleted} deleted`);
357+
log.info(`consolidation: ${updated} updated, ${deleted} deleted`);
357358
}
358359
}
359360
} catch (e) {
360-
console.error("[lore] consolidation error:", e);
361+
log.error("consolidation error:", e);
361362
}
362363

363364
// Prune temporal messages after distillation and curation have run.
@@ -371,12 +372,12 @@ export const LorePlugin: Plugin = async (ctx) => {
371372
maxStorageMB: cfg.pruning.maxStorage,
372373
});
373374
if (ttlDeleted > 0 || capDeleted > 0) {
374-
console.error(
375-
`[lore] pruned temporal messages: ${ttlDeleted} by TTL, ${capDeleted} by size cap`,
375+
log.info(
376+
`pruned temporal messages: ${ttlDeleted} by TTL, ${capDeleted} by size cap`,
376377
);
377378
}
378379
} catch (e) {
379-
console.error("[lore] pruning error:", e);
380+
log.error("pruning error:", e);
380381
}
381382

382383
// Export curated knowledge to AGENTS.md after distillation + curation.
@@ -387,7 +388,7 @@ export const LorePlugin: Plugin = async (ctx) => {
387388
exportToFile({ projectPath, filePath });
388389
}
389390
} catch (e) {
390-
console.error("[lore] agents-file export error:", e);
391+
log.error("agents-file export error:", e);
391392
}
392393
}
393394
},
@@ -508,8 +509,8 @@ export const LorePlugin: Plugin = async (ctx) => {
508509
break;
509510
}
510511
const dropped = result.messages.pop()!;
511-
console.error(
512-
"[lore] WARN: dropping trailing pure-text",
512+
log.warn(
513+
"dropping trailing pure-text",
513514
dropped.info.role,
514515
"message to prevent prefill error. id:",
515516
dropped.info.id,

src/log.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Lightweight logger that suppresses informational messages by default.
3+
*
4+
* In TUI mode, all stderr output renders as red "error" text — confusing
5+
* for routine status messages like "incremental distillation" or "pruned
6+
* temporal messages". Only actual errors should be visible by default.
7+
*
8+
* Set LORE_DEBUG=1 to see informational messages (useful when debugging
9+
* the plugin itself).
10+
*/
11+
12+
const isDebug = !!process.env.LORE_DEBUG;
13+
14+
/** Log an informational status message. Suppressed unless LORE_DEBUG=1. */
15+
export function info(...args: unknown[]): void {
16+
if (isDebug) console.error("[lore]", ...args);
17+
}
18+
19+
/** Log a warning. Suppressed unless LORE_DEBUG=1. */
20+
export function warn(...args: unknown[]): void {
21+
if (isDebug) console.error("[lore] WARN:", ...args);
22+
}
23+
24+
/** Log an error. Always visible — these indicate real failures. */
25+
export function error(...args: unknown[]): void {
26+
console.error("[lore]", ...args);
27+
}

0 commit comments

Comments
 (0)