feat(cli): add background cleanup for OpenAI API logs - #8862
Conversation
With model.enableOpenAILogging on, every API call appends a full request/response JSON under logs/openai with no rotation — heavy usage accumulates hundreds of thousands of files (tens of GB) within months. Register a third cleaner in the existing background housekeeping pipeline that sweeps openai-*.json files older than the new model.openAILogRetentionDays setting (default 7 days). The filename-embedded UTC date is used as a fast path to avoid one stat() per file; the boundary day and unparseable names fall back to mtime. Throttling is keyed on the resolved log dir, so both the default per-CWD layout and a shared custom openAILoggingDir are swept at most once a day. The sweep runs regardless of whether logging is currently enabled, so residue from earlier debugging sessions still gets cleaned. Scope note: housekeeping only starts for interactive sessions, so headless (-p) / SDK processes are not covered yet.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Thanks for the PR, @doudouOUC — the underlying problem is real and your description is detailed, but the PR body doesn't follow the PR template, so I have to pause it here. This is a formatting gate, not a code concern.
None of the template's required sections are present (the body currently uses a custom ## Summary / ## Changes / ## Test plan / ## Scope note structure):
- What this PR does — prose description of the change; note the template asks for prose, not a per-file rundown
- Why it's needed — motivation and user-facing benefit
- Reviewer Test Plan, with its three subsections:
- How to verify — the behaviors a reviewer should confirm and what to expect, not just the test commands you ran
- Evidence (Before & After) — e.g. a directory listing of
logs/openaibefore/after a sweep, since this change is about files on disk - Tested on — the OS matrix (🍏/🪟/🐧 with ✅/
⚠️ /N/A); right now it's unclear which OS your vitest/eslint/typecheck runs happened on
- Risk & Scope — the three bullets: main risk or tradeoff / not validated / breaking changes; your headless-session limitation note belongs under "Not validated / out of scope"
- Linked Issues —
Closes #8860
Could you restructure the body to follow the template? The content you already wrote is largely good — most of it can be moved into the right sections as-is. Please keep each paragraph or list item as one long line (the template notes that GitHub renders single newlines as <br>, so hard-wrapped text displays as a narrow column).
Once the body is updated, a maintainer can re-run triage with @qwen-code /triage to continue.
中文说明
感谢提交 PR,@doudouOUC —— 问题是真实存在的,描述也很详细,但 PR 正文没有遵循 PR 模板,所以需要先停在这里。这是一次格式上的拦截,而不是对代码的质疑。
模板要求的章节全部缺失(目前正文使用了自定义的 ## Summary / ## Changes / ## Test plan / ## Scope note 结构):
- What this PR does —— 用散文描述改动;注意模板要求散文描述,而不是按文件罗列
- Why it's needed —— 动机与用户收益
- Reviewer Test Plan,包含三个子章节:
- How to verify —— 评审者应确认的行为和预期结果,而不只是你跑过的测试命令
- Evidence (Before & After) —— 例如清扫前后
logs/openai的目录列表,因为这个改动针对的就是磁盘上的文件 - Tested on —— 操作系统矩阵(🍏/🪟/🐧 加 ✅/
⚠️ /N/A);目前无法判断你的 vitest/eslint/typecheck 是在哪个操作系统上运行的
- Risk & Scope —— 三个要点:主要风险或权衡 / 未验证项 / 破坏性变更;关于 headless 会话的局限应放在 "Not validated / out of scope" 下
- Linked Issues ——
Closes #8860
能否按模板重构正文?你已经写好的内容大部分是好的 —— 多数可以直接挪到对应章节。请保持每个段落或列表项为一长行(模板注明 GitHub 会把单个换行渲染成 <br>,硬换行的文字会显示成窄列)。
正文更新后,维护者可以用 @qwen-code /triage 重新触发 triage 继续流程。
— Qwen Code · qwen3.8-max
| // Matches the filenames OpenAILogger writes: | ||
| // `openai-<ISO timestamp>[_<diagnostic suffix>].json` — same predicate the | ||
| // reader side uses (see packages/cli/src/utils/sessionPaths.ts). | ||
| const OPENAI_LOG_FILE_PATTERN = /^openai-.*\.json$/; |
There was a problem hiding this comment.
This predicate accepts every openai-*.json file, while the sweep runs against the project-local default directory even when OpenAI logging is disabled. A user-owned old file such as logs/openai/openai-project-data.json is therefore deleted. Please match the logger’s exact timestamp/UUID filename contract, preferably through a shared ownership predicate or directory marker.
| | `model.skipStartupContext` | boolean | Skips sending the startup workspace context (environment summary and acknowledgement) at the beginning of each session. Enable this if you prefer to provide context manually or want to save tokens on startup. | `false` | | ||
| | `model.enableOpenAILogging` | boolean | Enables logging of OpenAI API calls for debugging and analysis. When enabled, API requests and responses are logged to JSON files. | `false` | | ||
| | `model.openAILoggingDir` | string | Custom directory path for OpenAI API logs. If not specified, defaults to `logs/openai` in the current working directory. Supports absolute paths, relative paths (resolved from current working directory), and `~` expansion (home directory). | `undefined` | | ||
| | `model.openAILogRetentionDays` | number | Days to retain OpenAI API log files written when `model.enableOpenAILogging` is on. Log files older than this are removed by a background housekeeping pass that runs at most once per day. `0` = minimum retention (~1 hour). Changes take effect after restart. | `7` | |
There was a problem hiding this comment.
The documented retention setting only runs through the interactive-only housekeeping startup path. Headless CLI and SDK processes can therefore continue producing OpenAI logs indefinitely. Please either cover those writers, or document the limitation here and keep the broader issue open until it is tracked separately.
qqqys
left a comment
There was a problem hiding this comment.
Requesting changes for the unresolved Critical data-deletion finding already documented in the current-head inline comment on packages/cli/src/utils/housekeeping/cleanup.ts:135.
yiliang114
left a comment
There was a problem hiding this comment.
The mechanics are right — boundary-day mtime disambiguation (strictly-older-than, both UTC), the 0/negative/NaN clamp to ~1h, top-level-only enumeration with isFile() (dirs/symlinks skipped), root dir never removed, per-dir sha256 marker so dir changes re-run, graceful skip on resolve failure, and no overlap with the session-layout cleaners — all verified correct and well pinned. But one P0 blocks:
The ownership predicate /^openai-.*.json$/ (cleanup.ts:135) is broader than the logger's actual filename contract (openai--<8-hex-uuid>[-].json per core/src/utils/openaiLogger.ts). Consequences, all verified: a date-less user file like openai-not-a-date.json in the log dir falls to the mtime fallback and is unlinked once >7 days old (the test actually pins this unsafe behavior as intended); a file like openai-2025-01-01-eval-data.json is deleted on filename string alone with no mtime check. Blast radius is amplified because the default dir /logs/openai lives inside the user's project checkout, the sweep runs even with logging disabled, and a custom openAILoggingDir may point at any user-chosen directory. The 'same predicate as the reader' justification doesn't transfer: sessionPaths.ts only reads. Fix: match the writer contract exactly (export the pattern from core alongside the existing UUID_PATTERN) and make unparseable names SKIP rather than mtime-delete. This matches the Critical qqqys already filed; the PR is one predicate away from approve.
Two smaller items: docs/settings.md:184 — retention only runs in interactive sessions (startup-prefetch.ts gates startBackgroundHousekeeping behind config.isInteractive()), so headless/SDK writers grow logs forever; document or track. And cleanup.ts:185 counts ENOENT-on-unlink as an error; treat it as benign success. Test fixtures also use a _suffix separator the writer never produces (writer uses -), and the load-bearing ownership test is currently pinned in reverse (look-alike user names deletable).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/config/settingsSchema.test.ts — no such file or directory; src/config/settings.test.ts — no such file or directory; 46 passed — this review observed 18781, 481 passed; 201 passed — this review observed 18781, 481 passed.
— qwen3.8-max via Qwen Code /review (v0.21.8)
wenshao
left a comment
There was a problem hiding this comment.
— Qwen Code via Qwen Code /review (v0.21.8)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Addressed the current review round in
Verification: 251 focused tests passed, affected-file ESLint passed, generated settings schema is current, and an independent smoke pass confirmed lookalike preservation, real-log removal, catch-up timing, and shared-directory policy order. The full build still stops only on the known Replied to every eligible unresolved thread; the two threads already authored by the PR author were skipped by the dedup guard. @qwen-code /triage |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 100 passed · 0 failed · 100 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:100 通过 · 0 失败 · 100 总计 Verification reportPR #8862 — feat(cli): add background cleanup for OpenAI API logsVerdict: 中文摘要
Central claim + A/BCentral claim: interactive housekeeping removes expired writer-owned OpenAI logs at most once per resolved dir per day, while preserving lookalikes, fresh files, directories, symlinks, and the log root. Fixture (identical in both cells): 2 expired writer-format logs (10d, 12d+suffix), 1 fresh writer log, expired
The base control differs only by the PR's code: Reviewer Test Plan, step by step
FindingsNo blocking findings. Non-blocking observations, for completeness:
Not covered
MethodologyEnvironment: Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
yiliang114
left a comment
There was a problem hiding this comment.
Re-review at head b6e45cd — the P0 is fixed and verified adversarially (13/13 name walk plus a real OpenAILogger write→delete regression test): the predicate now matches the writer contract exactly (ISO-ts with .sssZ, 8-hex uuid, optional sanitized suffix with leading/trailing-dash rule), everything non-matching is skipped, never mtime-deleted — openai-not-a-date.json and openai-2025-01-01-eval-data.json survive, real logger names (with and without suffix) are deletable when old, uppercase-UUID/9-hex/.json.bak variants rejected, boundary day still strictly-older-than via mtime. The sweep also hardened nicely: streaming opendir with bounded batches, ENOENT benign, non-ENOENT root-scan failure throws with the marker only written on success so the timer chain survives. All P2/P3 items addressed: interactive-only limitation documented in settings.md, both schema descriptions, and the new design doc; fixture separator matches the writer; ownership tests pin both directions (workspace-only retention on a custom dir skips with no marker). Non-blocking notes: the contract regex lives in cli with the single home being a cross-package regression test rather than an exported constant (deliberate, acceptable); custom-dir + workspace-scoped retention silently disables cleanup (fail-safe, documented); the EACCES test would not hold under root (runners are non-root). CI green on all runnable jobs. Ship it.
|
Thanks for the PR! Template looks good ✓ — the earlier stage-1a template gate is resolved by the rewritten body. Problem: observed, not theoretical. The linked issue #8860 documents ~342,000 files / ~95 GB accumulated in ~2 months of heavy use (~2–3 GB/day, disk reaching 97%), and the code confirms there is no rotation or retention anywhere on the OpenAI logging path. Direction: aligned. The change extends the existing background housekeeping pipeline — whose comment already reserved a slot for a debug-log cleaner — instead of adding new machinery. Supporting signal from claude-code's CHANGELOG: retention sweeps being extended to more file classes ("The Size: core paths touched — Approach: scope feels right. Reusing Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 先前的 stage-1a 模板关卡已被重写后的 PR 正文解决。 问题:已观测到的真实问题,不是理论加固。关联的 issue #8860 记录了重度使用约两个月累积 ~34.2 万个文件 / ~95 GB(约 2–3 GB/天,磁盘达 97%),代码确认 OpenAI 日志路径上没有任何轮转或保留机制。 方向:对齐。改动扩展现有后台 housekeeping 流水线(其注释本就为 debug-log 清理器预留了位置),而不是新增机制。参考信号:claude-code 的 CHANGELOG 中有同样的模式——保留期清理扩展到更多文件类别("The cleanupPeriodDays retention sweep now also covers ~/.claude/tasks/, ~/.claude/shell-snapshots/, and ~/.claude/backups/")。 规模:触及核心路径—— 方案:范围合理。复用 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。 进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewNo critical blockers. I re-verified the current head independently rather than relying on the prior review thread:
Two non-blocking gaps remain: the per-file error branch still has no direct test (the author declined adding a production injection seam solely for testing; the root-scan failure path is covered), and Test evidence — the PR's own CI at
|
| Check | Conclusion |
|---|---|
| Test (ubuntu-latest, Node 22.x) | ✅ success |
| Test (windows-latest, Node 22.x) | ⏭️ skipped |
| Test (macos-latest, Node 22.x) | ⏭️ skipped |
| Integration Tests (CLI, No Sandbox) | ⏭️ skipped |
| web-shell E2E Smoke (ubuntu-latest, Node 22.x) | ✅ success |
| Desktop Shell (ubuntu-22.04) | ✅ success |
| Desktop Shell (windows-2022) | ✅ success |
The Qwen Code CI workflow run (pull_request event) is completed: everything that ran is green, including the full unit suite with the 470 new test lines. The Windows/macOS unit matrix and integration tests were skipped for this fork PR, so cross-platform behavior rests on inspection plus the author's macOS run — flagging it rather than hiding it.
Sandboxed verification for the behavioral claims is already in flight on this triage trigger — its report will post in this thread. The claims it should pin: only exact-writer-format files older than retention are deleted while prefix lookalikes survive, and workspace-scoped retention on a custom directory skips cleanup without writing a marker. Not verified here: Windows/Linux runtime behavior (author tested on macOS only; CI matrix skipped).
中文说明
代码审查
无关键阻塞。本轮我独立复核了当前 head,而不是只依赖此前的评审线程:
- 删除谓词 vs writer 契约 ——
cleanup.ts中的正则与OpenAILogger.logInteraction的实际输出精确匹配:冒号替换为-的 ISO 时间戳、uuidv4().slice(0, 8)小写十六进制 ID、可选后缀的字符集与首尾去-规则同sanitizeDiagnosticSuffix一致。回归测试通过真实OpenAILogger写入并删除文件,writer 契约漂移会让测试失败。删除侧谓词比读取侧发现谓词(startsWith('openai-') && endsWith('.json'))更严格是正确的——共用读取侧谓词正是第一轮 P0 的成因。符号链接与目录通过 Dirent 的entry.isFile()排除,且unlink不会跟随符号链接。 - 失败语义 ——
runThrottledOnce仅在任务成功完成后写 marker(taskCompleted标志),且cleanupOldOpenAILogs对非ENOENT的 opendir 失败向上抛出,因此扫描失败会在下个周期重试,而不是压制清理 24 小时。清理过程中文件消失视为良性(吞掉ENOENT),其他单文件错误计数且不阻断后续批次。 - 共享的
getCutoffDate钳制 —— 新增的MIN_DATE_MS下限只改变手工编辑超大值的场景(旧代码产生 Invalid Date、本来也什么都不删);正常输入产生完全相同的 cutoff,现有 file-history 与 subagent 清理器不受影响。 - 配置接线 —— 用到的
LoadedSettings字段(system/user/workspace/systemDefaults、isTrusted)与真实类结构一致;Config.getWorkingDir()/getContentGeneratorConfig()存在;startup-prefetch.ts调用点未变。自定义目录要求用户/系统级拥有的保留期,可信工作区提供歧义策略时跳过且不写 marker;归属链读取正确,两种工作区顺序均有测试。 - Schema 新增完全沿用
cleanupPeriodDays先例(requiresRestart: true、minimum: 0、相同理由注释),vscode 伴侣 schema 一致。
剩余两处非阻塞缺口:单文件错误分支仍无直接测试(作者拒绝仅为测试引入生产注入缝;根扫描失败路径已覆盖);Windows 下对被占用文件 unlink 的行为退化为"计数错误、下周期重试",但 CI 矩阵被跳过,未实际运行(见下)。
测试证据 —— b6e45cd 上 PR 自己的 CI,通过 API 获取(未本地执行 PR 代码)
CI 表格见上。Qwen Code CI(pull_request 事件)已完成:实际运行的任务全绿,包含新增 470 行测试在内的完整单元测试通过。该 fork PR 的 Windows/macOS 单元矩阵与集成测试被跳过,跨平台行为依赖代码审查与作者的 macOS 运行——如实标注而非掩盖。行为类声明的沙箱验证已随本次 triage 触发在运行中,报告会发在本线程;它应钉住:只删除精确 writer 格式的过期文件、同名前缀文件幸存;自定义目录上的工作区级保留期跳过清理且不写 marker。此处未验证:Windows/Linux 运行时行为(作者仅在 macOS 测试,CI 矩阵被跳过)。
— Qwen Code · qwen3.8-max
Reviewed at b6e45cd568f29a9749b9e2765b5916b389610bd4 · re-run with @qwen-code /triage
|
Confidence: 4/5 — clean review across every stage; the only reservations are environmental (the fork PR's CI skips the Windows/macOS unit matrix) and one untested per-file error branch, neither blocking. Stepping back: this is what a good contribution looks like. The problem is observed and quantified (95 GB / 342k files, #8860), the fix rides the existing housekeeping pipeline instead of inventing parallel machinery, the deletion predicate is pinned to the writer contract by a real- @yiliang114's adversarial re-review at this head reached the same conclusion and stands as an approval. The outstanding 中文说明置信度:4/5 —— 各阶段审查均干净;仅有的保留是环境性的(fork PR 的 CI 跳过 Windows/macOS 单元矩阵)和一个未覆盖的单文件错误分支,均不阻塞。 整体来看:这是一次高质量的贡献。问题来自真实观测且有量化数据(95 GB / 34.2 万文件,#8860),修复复用了现有 housekeeping 流水线而非另起炉灶,删除谓词通过真实 @yiliang114 在同一 head 上的对抗性复审得出相同结论并已批准。尚未撤销的 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Local verification report (real build, real session, real API traffic)I built this PR and its merge-base into two separate bundles and exercised the cleaner the way a user actually hits it: a real interactive
Verdict: works as described. I did not find a correctness bug. Three non-blocking notes at the bottom. 1. End-to-end sweep, head vs. baseReal headless turn writes real logs → genuinely old logs minted through the real
2. Writer ↔ deleter contractThe real risk with a stricter-than-reader predicate is the opposite of over-deletion: a writer shape the predicate misses leaks forever. I drove the shipped 145 files written, 145 matched, 0 unmatched. 3. First-pass delay matrix
Same shot also covers the shared-custom-directory policy: with retention at workspace scope the sweep is skipped, nothing is deleted and no success marker is written; moving the same value to user scope cleans the directory ( 4. Scale — the case from #886065,000 flat files (60k expired / 5k fresh, 254 MB) in one directory, swept from inside a live session:
5. Concurrency, throttling, and
|
| Head | b6e45cd(feat/openai-logs-housekeeping) |
| Base | 9aec40f(与 main 的 merge-base) |
| 环境 | macOS 26.6 (arm64)、Node v24.18.1、仓库 0.21.8 |
| 手段 | 两套 npm ci + npm run bundle 目录 · mock OpenAI SSE 服务 · tmux TUI · QWEN_DEBUG_LOG_FILE=1 抓 [HOUSEKEEPING] 日志 |
结论:行为与描述一致,没有发现正确性缺陷。文末有三条不阻塞合并的建议。
1. 端到端清理,head 对比 base
真实 headless 轮次写出真实日志 → 用真实 OpenAILogger 在偏移时钟下铸造真正过期的日志 → 放入各类干扰文件 → 启动交互式会话并保持空闲,等追赶(catch-up)pass 触发。
- head:正好删除 5 个过期的 writer 自有日志(
openai-logs: removed=5 errors=0),其中包含-subagent-*与-side-query-*后缀形态。 - 保留:
openai-my-export.json、…-ZZZZZZZZ.json(非十六进制 id)、…deadbeef.json.bak、openai_2026-01-01.json、README.md、一个与日志同名的目录,以及两个新鲜日志。日志根目录本身也保留。 - base:同一份 fixture,删除列表为空,也没有
.openai-logs-cleanup-*marker——本 PR 之前确实完全没有这个清理器。
2. writer ↔ deleter 契约
比"误删"更危险的其实是反向:删除谓词漏掉某种 writer 形态,日志就会永久泄漏。我用真实 OpenAILogger 跑了 29 种 promptId 形态(内部 id、side-query: id、subagent a#b#c id、unicode、首尾连字符、120 字符长 id)× 5 个时钟,把它实际写出的每个文件名拿去匹配本 PR 的谓词:
写出 145 个文件,145 个全部命中,0 漏网。
3. 首次调度延迟矩阵
下表每一行 .file-history-cleanup 都是新鲜的,只有 per-directory 的 OpenAI marker 在变。这正是本 PR 修的问题。
| build | OpenAI marker | 实测 |
|---|---|---|
| head | 缺失 | first pass in 60s |
| head | 8 天前 | first pass in 60s |
| head | 新鲜 | first pass in 600s |
| base | 缺失 | first pass in 600s ← 积压被忽略 |
同一张截图还覆盖了共享自定义目录的策略:保留值放在 workspace scope 时清理被跳过,没有删除任何文件,也没有写成功 marker;把同一个值移到 user scope 后目录被正常清理(removed=2 errors=0)。
4. 规模验证 —— 对应 #8860 的场景
单目录 65,000 个扁平文件(60k 过期 / 5k 新鲜,254 MB),在活跃会话中清理:
- 单次 pass 删除 60,000 / 60,000,耗时约 8.3 秒,
errors=0,恰好留下 5,000 个新鲜文件,254 MB → 20 MB。 - 整个清理过程中进程 fd 数稳定在 14–15,有界并发生效。
- 值得一提:在 APFS 上边遍历
opendir流边 unlink 没有导致条目被跳过,一次 pass 就扫干净了。(这点很重要,因为漏扫的部分要等 24 小时后才会再试。)
5. 并发、节流与 retention: 0
- 三个会话同时启动、指向同一日志目录:一个执行清理(
removed=40),一个抢O_EXCL锁失败,一个发现 marker 新鲜。最终剩 0 文件、0 错误,没有重复 unlink 的噪音。 - 两个不同项目 → 两个独立的
.openai-logs-cleanup-<hash>marker,各自清理;同一项目再开会话会被节流(openai-logs-cleanup: skipping, ran …ms ago)。 retention: 0行为与文档一致:文件名是当天日期但 mtime 为 2 小时前的文件被删除,几分钟前写入的文件保留——即"截止当天用 mtime 兜底"这条分支在真实 writer 输出上确实生效。
6. 静态检查
两套目录 npm ci(含完整构建与 tsc --noEmit)退出码 0 · 所有改动文件 eslint 无告警 · 聚焦测试 97/97 通过(cleanup 24、scheduler 26、throttledOnce 7、settingsSchema 40)· 重新执行 npm run generate:settings-schema 无 diff,vscode schema 已同步。
另外补充一点:这里的完整构建并没有遇到 PR 描述中提到的 Ink selection 类型错误,在当前 merge-base 上是干净通过的。
不阻塞合并的建议
a) 非法的 openAILogRetentionDays 会静默变成约 1 小时,而不是回落到 7 天默认值。
-1 和 "abc" 都会落进 getCutoffDate 的 cleanupPeriodDays > 0 ? … : MS_PER_HOUR 分支,于是真实的 3 天前日志——明明在文档描述的窗口内——被无声删除。schema 里写了 minimum: 0,但运行时并没有强制。这是与 general.cleanupPeriodDays 共用的既有行为,不算回归;但既然这是一个会删数据的新设置,建议在合并前把非有限值/负值 clamp 回 DEFAULT_OPENAI_LOG_RETENTION_DAYS,而不是落到最激进的那条分支。
b) workspace scope 的跳过对用户不可见。 它只通过 debugLogger.error 记录,而这在未设置 QWEN_DEBUG_LOG_FILE 时不会写任何东西。受影响的用户既得不到清理,也得不到任何提示。此外,当 workspace 的值与 user/system scope 解析结果完全相同时也会触发跳过,属于安全但没必要的跳过。建议在 UI 中提示一次,或者只在值确实不同的时候才跳过。
c) getOpenAILogCleanupTarget() 每个会话被求值两次(getFirstPassDelay 一次、runHousekeeping 一次),所以 (b) 中的诊断日志每个会话会打印两遍。属于观感问题。
最后复述并确认作者自己列出的范围外事项:我用来生成日志的运行都是 -p,它完全不会启动 housekeeping。也就是说,靠脚本/headless 运行积累日志的用户,只有在同一目录开一次交互式会话之后才会被清理。
Fixtures, harness scripts and full logs kept locally; screenshots published to pr-assets/8862-verify.
|
Released in v0.21.9. |










What this PR does
This PR adds OpenAI API log retention to the existing interactive background-housekeeping pipeline. It removes expired flat-file logs at most once per resolved directory per day, uses the UTC date in writer-owned filenames to avoid a
statcall for every file, streams large directories with bounded deletion concurrency, and preserves the log directory itself. Deletion accepts only the exact timestamp-and-ID filename shape emitted byOpenAILogger, so unrelatedopenai-*.jsonfiles in project-local or custom directories are not touched.It adds
model.openAILogRetentionDayswith a default of seven days and a minimum-retention value of0(approximately one hour). The first-pass scheduler now considers the resolved OpenAI log marker as well as the file-history marker, so a missing or old per-directory marker gets the one-minute catch-up delay. Default per-workspace directories use the merged retention setting; a custom directory uses a user- or system-owned retention policy, and cleanup is skipped when a trusted workspace would otherwise supply an ambiguous directory-wide policy.Why it's needed
With
model.enableOpenAILoggingenabled, every OpenAI-compatible API call writes the full request and response to a new JSON file with no rotation or retention. Heavy use was observed to create approximately 342,000 files and 95 GB in two months, including full prompts and responses. A short dedicated retention window reduces disk, inode, and sensitive-data accumulation while preserving recent logs used for debugging and the in-tree latest-session lookup.Reviewer Test Plan
How to verify
openai-my-export.json; run housekeeping with seven-day retention and confirm only the expired writer-owned log is removed while the directory remains.0retains files younger than approximately one hour, two different directories receive independent throttle markers, and a non-ENOENTroot scan failure does not write a success marker.Evidence (Before & After)
Before: an expired user-owned
openai-my-export.jsonmatched the prefix-only predicate and was deleted (removed=1,existsAfter=false); a fresh file-history marker also forced a ten-minute initial delay even when the OpenAI marker was absent.After: the same lookalike remains, an expired file generated under the real
OpenAILoggercontract is removed, and missing or more-than-seven-day-old OpenAI markers select the one-minute catch-up delay. Focused configuration and housekeeping verification passes 251 tests.Tested on
Environment (optional)
macOS; Node.js 22.22.3; repository version 0.21.8; focused Vitest and ESLint checks. The full build reaches the CLI package and then hits the pre-existing Ink selection-type errors on
mainin untouched UI files.Risk & Scope
OpenAILoggerfilename so writer drift fails the cleaner suite. Custom directories favor safe, machine-owned retention over workspace-specific policies because their flat files do not encode workspace ownership.-p) and SDK-only processes do not start interactive housekeeping, so they can still accumulate logs; write-path cleanup remains follow-up work. Windows and Linux were not tested locally.model.openAILoggingDirwith workspace-scopedmodel.openAILogRetentionDaysmust move the retention value to user or system scope for cleanup to run.Linked Issues
Addresses #8860
中文说明
本 PR 的改动
本 PR 在现有的交互式后台 housekeeping 流水线中加入 OpenAI API 日志保留机制。它针对每个解析后的日志目录最多每天清理一次过期的扁平日志文件,利用 writer 自有文件名中的 UTC 日期避免为每个文件执行一次
stat,以有界删除并发流式扫描大目录,并始终保留日志根目录。删除只接受OpenAILogger实际生成的“时间戳 + ID”精确文件名格式,因此不会触碰项目目录或自定义目录中无关的openai-*.json文件。本 PR 新增
model.openAILogRetentionDays,默认值为 7 天,0表示约 1 小时的最短保留期。首次调度现在同时检查解析后的 OpenAI 日志 marker 和 file-history marker,因此缺失或过旧的目录级 marker 会使用 1 分钟追赶延迟。默认的工作区目录使用合并后的保留设置;自定义目录使用用户级或系统级拥有的保留策略,如果可信工作区会提供一个有歧义的目录级策略,则跳过清理。为什么需要
启用
model.enableOpenAILogging后,每次 OpenAI 兼容 API 调用都会把完整请求与响应写入新的 JSON 文件,当前没有轮转或保留机制。重度使用下曾观测到两个月约产生 34.2 万个文件、占用 95 GB,其中包含完整 prompt 和响应。独立且较短的保留窗口可以减少磁盘、inode 和敏感数据的累积,同时保留调试以及代码库内“查找当前会话最新日志”所需的近期文件。评审者测试计划
如何验证
openai-my-export.json);以 7 天保留期运行 housekeeping,确认只删除过期的 writer 自有日志,并保留目录本身。0会保留约 1 小时内的文件,两个不同目录拥有相互独立的节流 marker,且根目录发生非ENOENT扫描错误时不会写成功 marker。证据(改动前后)
改动前:过期的用户自有
openai-my-export.json会命中过宽的前缀谓词并被删除(removed=1、existsAfter=false);即使 OpenAI marker 缺失,只要 file-history marker 新鲜,首次调度仍会被延迟 10 分钟。改动后:同一个同名前缀文件会被保留,按照真实
OpenAILogger契约生成的过期文件会被删除,并且缺失或超过 7 天的 OpenAI marker 会选择 1 分钟追赶延迟。配置与 housekeeping 的聚焦验证共 251 个测试通过。测试平台
环境(可选)
macOS;Node.js 22.22.3;仓库版本 0.21.8;运行了聚焦 Vitest 与 ESLint 检查。完整构建到达 CLI 包后,在未改动的 UI 文件中遇到了
main已存在的 Ink selection 类型错误。风险与范围
OpenAILogger文件名,因此 writer 漂移会使清理器测试失败。由于自定义目录中的扁平文件不包含工作区归属,自定义目录优先采用安全的机器级保留策略,而不是工作区特有策略。-p)和仅 SDK 的进程不会启动交互式 housekeeping,因此仍可能累积日志;在写入路径上清理属于后续工作。Windows 和 Linux 未在本地测试。model.openAILoggingDir和工作区级model.openAILogRetentionDays的用户,需要把保留值移动到用户级或系统级,清理才会运行。关联问题
Addresses #8860