Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ grep -c 'your-feature-name' ~/.cache/opencode/packages/opencode-acp@latest/node_

```
~/.config/opencode/logs/acp/context/<session_id>/<timestamp>.json # per-request message snapshots
~/.config/opencode/logs/acp/daily/<date>.log # session load/save events
~/.config/opencode/logs/acp/daily/<date>.log # WARN/ERROR always; INFO/DEBUG when debug: true
```

### 3.5 npm Publishing
Expand Down
2 changes: 1 addition & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Status legend: **ACTIVE** = currently used | **DEPRECATED** = accepted but no ef
- **Type:** `boolean`
- **Default:** `false`
- **Status:** ACTIVE
- **Description:** Enable debug mode. When `true`, ACP sends a chat notification after each compression showing block details. Also enables per-request debug logs at `~/.config/opencode/logs/acp/`.
- **Description:** Enable debug mode. When `true`, ACP sends a chat notification after each compression showing block details, and enables INFO/DEBUG logs plus per-request context snapshots at `~/.config/opencode/logs/acp/`. WARN/ERROR lines are always written to `~/.config/opencode/logs/acp/daily/<date>.log` regardless of this flag.

#### `pruneNotification`
- **Type:** `"off" | "minimal" | "detailed"`
Expand Down
2 changes: 1 addition & 1 deletion CONFIGURATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ ACP 从最多三层配置文件中读取(后加载的覆盖先加载的):
- **类型:** `boolean`
- **默认值:** `false`
- **状态:** ACTIVE
- **说明:** 启用调试模式。设为 `true` 时,ACP 在每次压缩后发送聊天通知,显示块详情。同时启用按请求的调试日志,输出到 `~/.config/opencode/logs/acp/`。
- **说明:** 启用调试模式。设为 `true` 时,ACP 在每次压缩后发送聊天通知,显示块详情,并启用 INFO/DEBUG 日志与按请求的上下文快照(`~/.config/opencode/logs/acp/`)。无论此开关如何设置,WARN/ERROR 始终写入 `~/.config/opencode/logs/acp/daily/<日期>.log`。

#### `pruneNotification`
- **类型:** `"off" | "minimal" | "detailed"`
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ Each level overrides the previous, so project settings take priority over global
// Automatically update npm-installed ACP when a newer npm latest is available.
// Version-locked plugin specs are not updated.
"autoUpdate": true,
// Enable debug logging to ~/.config/opencode/logs/acp/
// Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
// (WARN/ERROR are always logged to daily/<date>.log)
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "off",
Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ ACP 使用自己的配置文件,按以下顺序搜索:
// Automatically update npm-installed ACP when a newer npm latest is available.
// Version-locked plugin specs are not updated.
"autoUpdate": true,
// Enable debug logging to ~/.config/opencode/logs/acp/
// Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
// (WARN/ERROR are always logged to daily/<date>.log)
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "detailed",
Expand Down
64 changes: 64 additions & 0 deletions devlog/2026-08-16_default-error-logging/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# REQ - Write ERROR/WARN logs by default (without debug: true)

- Task ID: `2026-08-16_default-error-logging`
- Home Repo: `opencode-acp`
- Created: 2026-08-16
- Status: Done
- Priority: P1
- Owner: ranxianglei
- References: user report — "用户说任务报错 我应该让他找什么日志"

## 1. Background & Problem Statement

- **Context**: `Logger` is constructed as `new Logger(config.debug)` (index.ts:35). With the default `debug: false`, `write()`, `saveContext()` and the level methods all early-return, so **no log file is created at all** under `~/.config/opencode/logs/acp`. When a user's task errors out, there is nothing on disk to diagnose from.
- **Current behavior (symptom)**: with default config, `~/.config/opencode/logs/acp` never exists; ERROR/WARN events ("Failed to load session state", "Failed to send notification", quality-gate failures, provider 400 related warnings) are silently discarded.
- **Expected behavior**: with default config, ERROR and WARN events are still appended to `~/.config/opencode/logs/acp/daily/<date>.log`; INFO/DEBUG and per-request context snapshots (`context/<sessionId>/*.json`) remain gated behind `debug: true`.
- **Impact**: users can now diagnose task failures from the daily log without enabling full debug (which also enables the heavy per-request context snapshots).

## 2. Reproduction (if applicable)

- **Environment**:
- Node: 22/24 (CI matrix)
- OS/Arch: linux-x64
- **Minimal reproduction steps**:
1) Run opencode with default `acp.jsonc` (no `debug` key → default `false`, lib/config.ts:176)
2) Trigger an ACP error path (e.g. corrupt `~/.local/share/opencode/storage/plugin/acp/<sessionId>.json` → "Failed to load session state")
3) Observe `~/.config/opencode/logs/acp` does not exist — no evidence on disk
- **Relevant configuration**:
```jsonc
// ~/.config/opencode/acp.jsonc
{ "debug": false } // default
```

## 3. Constraints & Non-Goals

- **Constraints**:
- Backward compatibility: no persisted-state or config-schema changes; only the logging gate changes. `debug: true` behavior is unchanged (all levels + context snapshots).
- Performance: daily-log append is a single small `writeFile` with `flag: "a"` — cost is negligible; WARN/ERROR call sites are all rare anomaly events (state load failures, notification failures, quality-gate failures, phantom batch entries), so default volume stays tiny.
- **Non-Goals** (explicitly out of scope):
- Promoting high-frequency DEBUG call sites (nudge injection, filter decisions, compression-start recording) to always-written INFO — that would bloat the daily log. They stay `debug`-gated.
- Writing per-request context snapshots by default (heavy; remains `debug`-gated).
- Adding new call sites for upstream (provider) API errors — opencode core owns those; this PR only changes the gate so existing ERROR/WARN sites become visible by default.
- Changing the version field in package.json (forbidden on non-release branches).

## 4. Acceptance Criteria (must be testable)

- **Correctness**:
- [ ] With `debug: false`: `logger.error(...)` and `logger.warn(...)` append a line to `~/.config/opencode/logs/acp/daily/<YYYY-MM-DD>.log`
- [ ] With `debug: false`: `logger.info(...)` and `logger.debug(...)` do NOT write
- [ ] With `debug: true`: all four levels write (unchanged behavior)
- **Performance / Stability**:
- [ ] Log line format unchanged: `<ISO timestamp> <LEVEL> <component>: <message> | <data> | v=<version>`
- **Regression**:
- [ ] New test file `tests/logger.test.ts` added and passing; full suite `npm run test` green; `npm run typecheck` and `npm run build` pass

## 5. Proposed Approach (optional)

- **Affected modules & entry files**:
- `lib/logger.ts` — move the enable gate into `write()` per level: gate becomes `if (!this.enabled && level !== "ERROR" && level !== "WARN") return`; drop the `enabled` early-return inside `warn()` and `error()` so they always flow to `write()`; keep it in `info()` and `debug()`.
- `tests/logger.test.ts` (new) — construct `Logger(false)` / `Logger(true)`, point `XDG_CONFIG_HOME` at a temp dir, await each level, assert daily-log file contents.
- **Risks**:
- WARN volume: all WARN sites are anomaly paths; worst case a few lines per session — acceptable.
- `write()` silently swallows FS errors (existing `catch (error) {}`) — behavior unchanged.
- **Rollback strategy**:
- Revert the single logger.ts hunk; no state or API changes to unwind.
83 changes: 83 additions & 0 deletions devlog/2026-08-16_default-error-logging/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# WORKLOG - Write ERROR/WARN logs by default (without debug: true)

- Task ID: `2026-08-16_default-error-logging`
- Home Repo: `opencode-acp`
- Status: Done
- Updated: 2026-08-16 04:30

## 1. Summary

- **What was done** (1–3 sentences): Changed `Logger` so that ERROR and WARN lines are appended to the daily log even when `debug` is off; INFO/DEBUG and per-request context snapshots remain gated behind `debug: true`.
- **Why** (1–3 sentences): With the default `debug: false`, no ACP log file was ever created, leaving users with zero on-disk evidence when a task errors. Making only the rare ERROR/WARN anomaly events write by default gives a minimal but useful diagnostic trail without the volume of full debug mode.
- **Behavior / compatibility changes**: Yes — new behavior: `~/.config/opencode/logs/acp/daily/<date>.log` now receives ERROR/WARN lines by default. No persisted-state, config-schema, or exported-API changes.
- **Risk level**: Low

## 2. Change Log

### Commits

| Commit | Description |
|--------|-------------|
| `2ccbf25` | fix: write ERROR/WARN to daily log even when debug is off |
| `d2eb475` | test: restore prior XDG_CONFIG_HOME + per-test date in logger tests (dual-review P3 fixes) |

### Key Files

- `lib/logger.ts` — `write()` gate changed from `if (!this.enabled) return` to a level-aware gate (`if (!this.enabled && level !== "ERROR" && level !== "WARN") return`); `warn()`/`error()` no longer early-return when disabled.
- `tests/logger.test.ts` — new test file covering disabled/enabled logger across all four levels (4 tests).

## 3. Design & Implementation Notes

- **Entry point / key function**: `Logger.write()` (lib/logger.ts:75) is the single write path; gating happens there per level.
- **Key configuration items**: `debug` (lib/config.ts:176 default `false`) still gates INFO/DEBUG + context snapshots; no new config key added.
- **Key logic explanation** (if non-trivial): `write(level, ...)` now returns early only when disabled AND level is neither ERROR nor WARN. `info()`/`debug()` keep their own early-return; `warn()`/`error()` dropped theirs so they always reach `write()`.

## 4. Testing & Verification

### Build & Test Commands

```sh
# Type check
npx tsc --noEmit

# Build
cd opencode-acp && npm run build

# Run full test suite
node --import tsx --test tests/*.test.ts

# Run specific test file
node --import tsx --test tests/logger.test.ts
```

### Test Coverage

- New/modified test files: `tests/logger.test.ts` (new, 4 tests)
- Test count: 980 total, 980 pass, 0 fail (full suite, ~25.3s)
- Key scenarios verified:
- disabled logger: ERROR + WARN lines appended to daily log; INFO/DEBUG not written; log file absent before first error/warn
- enabled logger: all four levels write
- line format: `<ISO ts> <LEVEL> <component>: <msg> | <data> | v=<version>`

### Results

- **PASS/FAIL**: PASS — `npx tsc --noEmit` clean; `npm run build` OK (dist/index.js 391.32 KB); full suite 980/980
- **Key logs/data**: `node --import tsx --test tests/logger.test.ts` → 4 pass; full suite → 980 pass, 0 fail

## 5. Risk Assessment & Rollback

- **Risk points**: WARN call sites are all anomaly paths (state load/save failures, notification failures, quality-gate failures, phantom batch entries) — default daily-log volume stays tiny. `write()` still swallows FS errors silently (pre-existing `catch {}`, unchanged).
- **Rollback method**:
- Revert commit(s): `2ccbf25`
- Rollback impact: none — no state or schema changes to unwind.
- **Compatibility notes** (data format, config schema): No

## 6. Lessons Learned (optional)

- What went well: single-gate design in `write()` keeps the decision in one place; tests isolate the log dir via `XDG_CONFIG_HOME`.
- What could be improved: dual-agent review (LoggerReviewerA + TestReviewerB on PR #311) flagged P3 items — WARN amplification on corrupted state file (accepted, documented), per-call getCallerFile cost (negligible), REQ status mismatch (fixed: REQ now `Done`), env restore-instead-of-delete (fixed in tests/logger.test.ts: `withConfigHome` helper), midnight date flake (fixed: date computed inside `setup()`).
- Reusable conclusions: level-based gating is cheaper and safer than promoting debug sites to info; keep high-frequency DEBUG sites gated.

## 7. Follow-ups (optional)

- [ ] Consider an opt-out config key (e.g. `logs: { minimal: true }`) if WARN volume ever becomes an issue
6 changes: 3 additions & 3 deletions lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export class Logger {
}

private async write(level: string, component: string, message: string, data?: any) {
if (!this.enabled) return
// ERROR and WARN are written even when debug logging is off, so that
// failures remain visible in the daily log by default.
if (!this.enabled && level !== "ERROR" && level !== "WARN") return

try {
await this.ensureLogDir()
Expand Down Expand Up @@ -106,13 +108,11 @@ export class Logger {
}

warn(message: string, data?: any) {
if (!this.enabled) return
const component = this.getCallerFile(2)
return this.write("WARN", component, message, data)
}

error(message: string, data?: any) {
if (!this.enabled) return
const component = this.getCallerFile(2)
return this.write("ERROR", component, message, data)
}
Expand Down
16 changes: 11 additions & 5 deletions lib/messages/filter/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export function applyMessageFilters(

const result: ApplyResult = { partsFiltered: 0, partsDropped: 0, partsModified: 0 }
const total = messages.length
// WARN reaches the daily log even with debug off, and a broken filter throws
// for every message×part — report each filter once per pass, not per message.
const warnedFilters = new Set<string>()

const buildCtx = (text: string, role: string, i: number): MessageFilterContext => ({
text,
Expand Down Expand Up @@ -89,11 +92,14 @@ export function applyMessageFilters(
try {
decision = filter.filter(filterCtx)
} catch (err) {
logger.warn("Message filter threw error", {
filter: filter.name,
error: err instanceof Error ? err.message : String(err),
messageIndex: i,
})
if (!warnedFilters.has(filter.name)) {
warnedFilters.add(filter.name)
logger.warn("Message filter threw error", {
filter: filter.name,
error: err instanceof Error ? err.message : String(err),
messageIndex: i,
})
}
continue
}
if (decision.action === "keep") continue
Expand Down
1 change: 1 addition & 0 deletions tests/compress-range-placeholders.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./test-env"
import assert from "node:assert/strict"
import test from "node:test"
import type { CompressionBlock } from "../lib/state"
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e-tier-compression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ test("applyCompressionState: T1 block gets effectiveCompressedTokens = compresse
test("tier-aware decompress: default restores one level up (T2→T1)", async () => {
const tmpDir = mkdtempSync(join(tmpdir(), "acp-tier-decomp-"))
const registry = createTestRegistry(tmpDir)
const logger = new Logger({ level: "error" })
const logger = new Logger(false)
const config = buildConfig()

const t1 = makeCompressionBlock(1, 1000, "T1 work", 1, 10, "u1")
Expand Down Expand Up @@ -822,7 +822,7 @@ test("tier-aware decompress: default restores one level up (T2→T1)", async ()
test("tier-aware decompress: full:true restores to original (T2→raw)", async () => {
const tmpDir = mkdtempSync(join(tmpdir(), "acp-tier-decomp-full-"))
const registry = createTestRegistry(tmpDir)
const logger = new Logger({ level: "error" })
const logger = new Logger(false)
const config = buildConfig()

const t1 = makeCompressionBlock(1, 1000, "T1 work", 1, 10, "u1")
Expand Down
1 change: 1 addition & 0 deletions tests/gc-merge.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./test-env"
import assert from "node:assert/strict"
import test from "node:test"
import { mergeMarkedBlocks, runBatchCleanup } from "../lib/gc/merge"
Expand Down
1 change: 1 addition & 0 deletions tests/hooks-permission.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./test-env"
import assert from "node:assert/strict"
import test from "node:test"
import type { PluginConfig } from "../lib/config"
Expand Down
1 change: 1 addition & 0 deletions tests/inject.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./test-env"
import assert from "node:assert/strict"
import test from "node:test"
import * as fs from "fs/promises"
Expand Down
Loading
Loading