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
8 changes: 7 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ 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, 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.
- **Description:** Enable debug mode. When `true`, ACP sends a chat notification after each compression showing block details, and sets `logLevel` to `debug` (INFO/DEBUG logs plus per-request context snapshots at `~/.config/opencode/logs/acp/`). This flag overrides `logLevel` when set to `true`.

#### `logLevel`
- **Type:** `"debug" | "info" | "warn" | "error" | "silent"`
- **Default:** `"info"`
- **Status:** ACTIVE
- **Description:** File log verbosity for `~/.config/opencode/logs/acp/daily/<date>.log`. Default `info` writes decision-level events by default (nudge decisions, transform summaries, auto-update checks, model switches). `warn`/`error` reduce output; `silent` disables file logging entirely; `debug` additionally enables per-request context snapshots and verbose dumps. Ignored when `debug: true`.

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

#### `logLevel`
- **类型:** `"debug" | "info" | "warn" | "error" | "silent"`
- **默认值:** `"info"`
- **状态:** ACTIVE
- **说明:** 文件日志详细级别(`~/.config/opencode/logs/acp/daily/<日期>.log`)。默认 `info`:默认落盘决策级事件(压缩提示决策、转换摘要、自动更新检查、模型切换等)。`warn`/`error` 减少输出;`silent` 完全关闭文件日志;`debug` 额外启用按请求的上下文快照与详细转储。`debug: true` 时忽略此配置。

#### `pruneNotification`
- **类型:** `"off" | "minimal" | "detailed"`
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,11 @@ Each level overrides the previous, so project settings take priority over global
// on the installed dist-tag/spec (@stable follows stable, @latest follows latest).
// Version-locked plugin specs are not updated.
"autoUpdate": true,
// Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
// (WARN/ERROR are always logged to daily/<date>.log)
// File log verbosity: "debug" | "info" | "warn" | "error" | "silent".
// Default "info" writes decision-level events (nudges, transforms, updates)
// to ~/.config/opencode/logs/acp/daily/<date>.log
"logLevel": "info",
// Enable full DEBUG logging + per-request snapshots (overrides logLevel)
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "off",
Expand Down
7 changes: 5 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,11 @@ ACP 使用自己的配置文件,按以下顺序搜索:
// 自动更新 npm 安装的 ACP:跟踪安装所用 dist-tag/规范(@stable 跟随 stable,@latest 跟随 latest)。
// 版本锁定的规范不会被更新。
"autoUpdate": true,
// Enable INFO/DEBUG logging + per-request snapshots to ~/.config/opencode/logs/acp/
// (WARN/ERROR are always logged to daily/<date>.log)
// 文件日志级别: "debug" | "info" | "warn" | "error" | "silent"
// 默认 "info":决策级事件(压缩提示、转换摘要、更新检查)落盘到
// ~/.config/opencode/logs/acp/daily/<日期>.log
"logLevel": "info",
// 启用完整 DEBUG 日志 + 按请求快照(优先于 logLevel)
"debug": false,
// Notification display: "off", "minimal", or "detailed"
"pruneNotification": "detailed",
Expand Down
8 changes: 7 additions & 1 deletion dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
"debug": {
"type": "boolean",
"default": false,
"description": "Enable debug logging"
"description": "Enable full debug logging (overrides logLevel)"
},
"logLevel": {
"type": "string",
"enum": ["debug", "info", "warn", "error", "silent"],
"default": "info",
"description": "Log verbosity: decision-level events (info) are written by default. debug adds per-message detail; warn/error/silent reduce output."
},
"pruneNotification": {
"type": "string",
Expand Down
25 changes: 25 additions & 0 deletions devlog/2026-08-21_default-on-logging/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# REQ — default-on decision-level logging

## 背景

用户反馈:项目日志非常少,且只在 `debug: true` 时才写 INFO/DEBUG。默认配置(`debug: false`)下只有 WARN/ERROR 落盘,出问题时文件里几乎没有可排查的决策轨迹。

## 需求

1. **默认(非 debug)就要打印**:INFO 级决策日志默认写盘。
2. 可配置:用户能把级别调低(warn/error/silent)或调高(debug)。
3. 向后兼容:
- `new Logger(boolean)` 现有语义不变(false→仅 WARN/ERROR;true→全量),~40 个测试文件不受影响。
- `debug: true` 行为不变(debug 级 + saveContext 快照)。

## 方案

- `LogLevel = "debug" | "info" | "warn" | "error" | "silent"` + LEVEL_RANK 门控写入。
- 新配置项 `logLevel`(默认 `"info"`),`debug: true` 时覆盖为 debug。
- INFO 决策日志铺到核心路径:插件初始化、每请求 transform 摘要、模型切换、nudge 注入/抑制决策(含 tier 触发)、自动更新检查全程。
- saveContext 仍只在 debug 级开启(避免默认每请求 JSON 转储膨胀)。

## 验收

- 默认配置下 `~/.config/opencode/logs/acp/daily/<date>.log` 有 INFO 决策行。
- `tsc --noEmit` 0 错误;全量测试通过(含新增 4 个分级语义测试)。
41 changes: 41 additions & 0 deletions devlog/2026-08-21_default-on-logging/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# WORKLOG — default-on decision-level logging

## 变更

### lib/logger.ts
- 新增 `export type LogLevel = "debug"|"info"|"warn"|"error"|"silent"` 与 `LEVEL_RANK`(silent=99 吞掉一切)。
- 构造签名 `constructor(enabled: boolean, level?: LogLevel)`;`this.level = level ?? (enabled ? "debug" : "warn")` —— 纯布尔调用保持旧语义。
- `get enabled()` 改为派生视图(`level === "debug"`)。
- `write()` 按级别秩次门控;`info()/debug()` 增加可选 `data` 参数(`unknown`,非 any)。
- `saveContext` 仅在 debug 级写快照。

### 配置链
- `lib/config.ts`:`PluginConfig.logLevel: LogLevel`,默认 `"info"`,mergeLayer 合并。
- `lib/config-validation.ts`:VALID_CONFIG_KEYS + 枚举校验(5 值)。
- `dcp.schema.json`:logLevel string enum,default "info"。

### 装配
- `index.ts`:`new Logger(config.debug, config.debug ? "debug" : config.logLevel)`;初始化 INFO(版本/工作区/级别/secure/auto-update);`startAutoUpdate(ctx, config.autoUpdate, logger)`。

### INFO 决策日志铺设
- `lib/messages/inject/inject.ts`:nudge 注入(trigger=tier/growth/emergency + usagePct + growthFloor + 推荐区间)、紧急 /compact 通知、抑制原因(all_protected / in_protected_zone / below_effective_floor)、tier 触发注入。
- `lib/hooks.ts`:每请求 transform 完成摘要(模型/消息数/pre-post tokens/上下文占用%/是否 nudge);会话内模型切换。
- `lib/update.ts`:线程化可选 Logger;检查启动/跳过原因(无包目录、不可读、无目标、规范不可更新、tag 无版本、已最新)/发现新版本/失败/应用成功。

### 测试
- `tests/logger.test.ts` 新增 4 例:显式 info 级(INFO/WARN/ERROR 写、DEBUG 门控)、silent 全吞、error 级仅 ERROR、`enabled` getter 反映显式级别。8/8 通过。

### 文档
- CONFIGURATION.md / CONFIGURATION.zh-CN.md:新增 `logLevel` 小节,重写 `debug` 小节(覆盖关系)。
- README.md / README.zh-CN.md:配置示例加 `logLevel`。

## 验证

- `npx tsc --noEmit` → 0 errors。
- `node --import tsx --test tests/logger.test.ts` → 8 pass / 0 fail。
- 全量:见 PR 检查项(发布前跑)。

## 风险与回滚

- 日志量增长有限:决策级事件每请求 ~2 行 + 低频路径;无 per-message 转储。
- 回滚点:本 PR 单 commit revert 即可。
14 changes: 12 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** ACP version, injected at build time by tsup define */
declare const ACP_VERSION: string | undefined
import type { Plugin } from "@opencode-ai/plugin"
import { getConfig } from "./lib/config"
import {
Expand Down Expand Up @@ -32,7 +34,15 @@ const server: Plugin = (async (ctx) => {
return {}
}

const logger = new Logger(config.debug)
const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel)
logger.info("ACP plugin initialized", {
version: typeof ACP_VERSION !== "undefined" ? ACP_VERSION : "dev",
workspace: ctx.directory,
logLevel: logger.level,
debug: config.debug,
autoUpdate: config.autoUpdate,
secureMode: isSecureMode(),
})
const registry = new SessionStateRegistry(logger)
const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts)
const hostPermissions: HostPermissionSnapshot = {
Expand Down Expand Up @@ -75,7 +85,7 @@ const server: Plugin = (async (ctx) => {

logger.info("DCP initialized")

startAutoUpdate(ctx, config.autoUpdate)
startAutoUpdate(ctx, config.autoUpdate, logger)

const compressToolContext = {
client: ctx.client,
Expand Down
1 change: 1 addition & 0 deletions lib/config-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const VALID_CONFIG_KEYS = new Set([
"enabled",
"autoUpdate",
"debug",
"logLevel",
"showUpdateToasts",
"allowSubAgents",
"pruneNotification",
Expand Down
5 changes: 5 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { homedir } from "os"
import { parse } from "jsonc-parser/lib/esm/main.js"
import type { PluginInput } from "@opencode-ai/plugin"
import { VALID_CONFIG_KEYS, getInvalidConfigKeys, validateConfigTypes, type ValidationError } from "./config-validation"
import type { LogLevel } from "./logger"


type Permission = "ask" | "allow" | "deny"
Expand Down Expand Up @@ -84,6 +85,8 @@ export interface PluginConfig {
enabled: boolean
autoUpdate: boolean
debug: boolean
/** Log verbosity when `debug` is false; `debug: true` forces full debug logging. Default: "info". */
logLevel: LogLevel
allowSubAgents: boolean
pruneNotification: "off" | "minimal" | "detailed"
pruneNotificationType: "chat" | "toast"
Expand Down Expand Up @@ -174,6 +177,7 @@ const defaultConfig: PluginConfig = {
enabled: true,
autoUpdate: true,
debug: false,
logLevel: "info",
allowSubAgents: true,
pruneNotification: "off",
// [FIX #20] Default to toast — chat-mode notifications inject an empty
Expand Down Expand Up @@ -498,6 +502,7 @@ function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConf
enabled: data.enabled ?? config.enabled,
autoUpdate: data.autoUpdate ?? config.autoUpdate,
debug: data.debug ?? config.debug,
logLevel: data.logLevel ?? config.logLevel,
allowSubAgents: data.allowSubAgents ?? data.experimental?.allowSubAgents ?? config.allowSubAgents,
pruneNotification: data.pruneNotification ?? config.pruneNotification,
pruneNotificationType: data.pruneNotificationType ?? config.pruneNotificationType,
Expand Down
23 changes: 23 additions & 0 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function createChatMessageTransformHandler(
requestModel?.providerID,
requestModel?.modelID,
)
const prevModelID = state.modelID
if (requestModelLimit !== undefined) {
state.modelContextLimit = requestModelLimit
state.modelProviderID = requestModel?.providerID
Expand All @@ -214,6 +215,14 @@ export function createChatMessageTransformHandler(
state.modelProviderID = requestModel.providerID
state.modelID = requestModel.modelID
}
if (requestModel?.modelID && requestModel.modelID !== prevModelID) {
logger.info("Model switched mid-session", {
session: state.sessionId,
from: prevModelID,
to: requestModel.modelID,
contextLimit: state.modelContextLimit,
})
}
await updatePerTurnState(state, logger, messages)
}

Expand Down Expand Up @@ -282,6 +291,20 @@ export function createChatMessageTransformHandler(
hideFailedCompressCalls(output.messages)
stripStaleMetadata(output.messages)
dropEmptyMessages(output.messages)
const postTokens = getCurrentTokenUsage(state, output.messages)
logger.info("Chat transform complete", {
session: state.sessionId,
model: state.modelID,
messages: output.messages.length,
prePruneTokens,
postTokens,
contextLimit: state.modelContextLimit,
usagePct:
postTokens !== undefined && state.modelContextLimit
? `${((postTokens / state.modelContextLimit) * 100).toFixed(1)}%`
: undefined,
nudged: state.nudges.shouldInjectThisTurn,
})

if (state.sessionId) {
await logger.saveContext(state.sessionId, output.messages)
Expand Down
63 changes: 50 additions & 13 deletions lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,53 @@ declare const ACP_VERSION: string | undefined

const LOG_VERSION = typeof ACP_VERSION !== "undefined" ? ACP_VERSION : "dev"

/**
* Log verbosity. Production default is `info` — decision-level events
* (nudge tiers, compression runs, gate verdicts, config resolution) are
* written to the daily log WITHOUT requiring `debug: true`. `debug` adds
* per-message detail and per-request context snapshots; `warn`/`error`
* reduce to failures only; `silent` disables file output entirely.
*/
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent"

const LEVEL_RANK: Record<LogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40,
silent: 99,
}

export class Logger {
private logDir: string
public enabled: boolean
/** Resolved verbosity; replaces the old boolean `enabled` flag. */
public readonly level: LogLevel

constructor(enabled: boolean) {
this.enabled = enabled
/**
* @param enabled legacy boolean toggle — `true` maps to `debug`,
* `false` maps to `warn` (errors + warnings only).
* @param level explicit verbosity; when given it wins over `enabled`.
*/
constructor(enabled: boolean, level?: LogLevel) {
// Config validation is warn-only, so an invalid `logLevel` string can
// reach us at runtime. Unknown levels must never zero out logging
// (LEVEL_RANK[x] === undefined fails every gate, dropping even ERROR);
// fall back to the legacy boolean mapping instead.
const resolved = level !== undefined && LEVEL_RANK[level] !== undefined ? level : undefined
this.level = resolved ?? (enabled ? "debug" : "warn")
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
this.logDir = join(configHome, "opencode", "logs", "acp")
}

/** Back-compat view of the old flag: true only at full debug verbosity. */
get enabled(): boolean {
return this.level === "debug"
}

private shouldWrite(level: LogLevel): boolean {
return LEVEL_RANK[level] >= LEVEL_RANK[this.level]
}

private async ensureLogDir() {
if (!existsSync(this.logDir)) {
await mkdir(this.logDir, { recursive: true })
Expand Down Expand Up @@ -72,10 +109,10 @@ export class Logger {
}
}

private async write(level: string, component: string, message: string, data?: any) {
// 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
private async write(level: string, component: string, message: string, data?: unknown) {
// Levels below the resolved verbosity are dropped here; WARN/ERROR
// still land by default because the production default is `info`.
if (!this.shouldWrite(level.toLowerCase() as LogLevel)) return

try {
await this.ensureLogDir()
Expand All @@ -95,14 +132,14 @@ export class Logger {
} catch (error) {}
}

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

debug(message: string, data?: any) {
if (!this.enabled) return
debug(message: string, data?: unknown) {
if (!this.shouldWrite("debug")) return
const component = this.getCallerFile(2)
return this.write("DEBUG", component, message, data)
}
Expand Down Expand Up @@ -215,8 +252,8 @@ export class Logger {
})
}

async saveContext(sessionId: string, messages: any[]) {
if (!this.enabled) return
async saveContext(sessionId: string, messages: unknown[]) {
if (this.level !== "debug") return

try {
const contextDir = join(this.logDir, "context", sessionId)
Expand Down
Loading
Loading