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
56 changes: 56 additions & 0 deletions docs/plans/memory-diagnostics-reference-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Memory Diagnostics Reference Design

## Context

Issue #3000 tracks memory and performance diagnostics for long-running Qwen
Code sessions. The first PR should establish a small, low-risk diagnostic
surface before adding heavier profiling or retention changes.

The design is reference-first:

- Claude Code keeps memory diagnostics separate from heap snapshot generation.
Its diagnostics include process memory, V8 heap statistics, heap spaces,
resource usage, active handles/requests, file descriptors, Linux
`smaps_rollup`, and leak hints.
- Codex focuses heavily on bounded retention and lazy loading for long-lived
process state. Those ideas should guide later PRs that address conversation,
command output, and history retention.

## First PR Scope

Add a `/doctor memory` diagnostic path that captures a single point-in-time
snapshot:

- `process.memoryUsage()`
- V8 heap statistics and heap spaces
- `process.resourceUsage()`
- active handle/request counts
- open file descriptor count when `/proc/self/fd` is available
- Linux `smaps_rollup` when available
- basic risk hints for heap pressure, detached contexts, excessive handles,
excessive requests, high file descriptor count, and native memory pressure

This command should be cheap enough to run in normal sessions and safe on
platforms where Linux-only probes are unavailable.

## Non-Goals

This PR intentionally does not:

- write heap snapshots
- run continuous polling
- change prompt/history retention
- change tool output retention
- alter module loading behavior

Those are follow-up PRs after the diagnostic baseline exists.

## Follow-Up PRs

1. Add explicit snapshot/export support for deeper local investigation.
2. Add bounded retention for large command/tool outputs, using Codex's capped
output retention as the main reference.
3. Audit lazy loading and module startup paths after measurements identify
hot spots.
4. Add repeatable memory/performance benchmark scenarios for long-running
sessions.
65 changes: 65 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,71 @@ describe('Session', () => {
});
});

it('honors explicit no-input override for built-in commands with subCommands', async () => {
getAvailableCommandsSpy.mockResolvedValueOnce([
{
name: 'doctor',
description: 'Run installation and environment diagnostics',
kind: 'built-in',
acceptsInput: false,
subCommands: [
{
name: 'memory',
description: 'Show current process memory diagnostics',
kind: 'built-in',
},
],
},
]);

await session.sendAvailableCommandsUpdate();

expect(mockClient.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: 'test-session-id',
update: expect.objectContaining({
sessionUpdate: 'available_commands_update',
availableCommands: expect.arrayContaining([
expect.objectContaining({
name: 'doctor',
description: 'Run installation and environment diagnostics',
input: null,
}),
]),
}),
}),
);
});

it('honors explicit input override for built-in commands without input metadata', async () => {
getAvailableCommandsSpy.mockResolvedValueOnce([
{
name: 'diagnostics',
description: 'Run diagnostics',
kind: 'built-in',
acceptsInput: true,
},
]);

await session.sendAvailableCommandsUpdate();

expect(mockClient.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: 'test-session-id',
update: expect.objectContaining({
sessionUpdate: 'available_commands_update',
availableCommands: expect.arrayContaining([
expect.objectContaining({
name: 'diagnostics',
description: 'Run diagnostics',
input: { hint: '' },
}),
]),
}),
}),
);
});

it('attaches available skills to available_commands_update metadata', async () => {
getAvailableCommandsSpy.mockResolvedValueOnce([
{
Expand Down
22 changes: 13 additions & 9 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1378,17 +1378,21 @@ export class Session implements SessionContext {
// let users type arguments before submitting. Commands with no argument
// support get input: null so the client auto-submits them on selection.
//
// A command is considered to accept arguments when any of:
// - it is not a BUILT_IN command (skills, file commands, etc.)
// - it has a completion function
// - it declares an argumentHint
// - it has subCommands
// acceptsInput is determined by:
// 1. cmd.acceptsInput, if explicitly set (true or false overrides
// inference)
// 2. Otherwise, a command accepts arguments when any of:
// - it is not a BUILT_IN command (skills, file commands, etc.)
// - it has a completion function
// - it declares an argumentHint
// - it has subCommands
const availableCommands: AvailableCommand[] = slashCommands.map((cmd) => {
const acceptsInput =
cmd.kind !== CommandKind.BUILT_IN ||
cmd.completion != null ||
cmd.argumentHint != null ||
(cmd.subCommands != null && cmd.subCommands.length > 0);
cmd.acceptsInput ??
(cmd.kind !== CommandKind.BUILT_IN ||
cmd.completion != null ||
cmd.argumentHint != null ||
(cmd.subCommands != null && cmd.subCommands.length > 0));
return {
name: cmd.name,
description: cmd.description,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,8 @@ export default {

// === Core: added from PR #3328 ===
'Open the memory manager.': 'Open the memory manager.',
'Show current process memory diagnostics':
'Show current process memory diagnostics',
'Save a durable memory to the memory system.':
'Save a durable memory to the memory system.',
'Ask a quick side question without affecting the main conversation':
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ export default {

// === Core: added from PR #3328 ===
'Open the memory manager.': '打開記憶管理器。',
'Show current process memory diagnostics': '顯示目前程序的內存診斷。',
'Save a durable memory to the memory system.': '將持久記憶保存到記憶系統。',
'Ask a quick side question without affecting the main conversation':
'在不影響主對話的情況下快速提問旁支問題',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,7 @@ export default {
'[{{label}}] failed: {{error}}': '[{{label}}] 失败:{{error}}',
'Loading suggestions...': '正在加载建议...',
'Open the memory manager.': '打开记忆管理器。',
'Show current process memory diagnostics': '显示当前进程的内存诊断。',
'Save a durable memory to the memory system.':
'将一条持久记忆保存到记忆系统。',
'Show per-item context usage breakdown.': '显示按项目划分的上下文使用详情。',
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/nonInteractiveCliCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ export type NonInteractiveSlashCommandResult =
}
| {
type: 'message';
messageType: 'info' | 'error';
messageType: 'info' | 'warning' | 'error';
content: string;
}
| {
type: 'stream_messages';
messages: AsyncGenerator<
{ messageType: 'info' | 'error'; content: string },
{ messageType: 'info' | 'warning' | 'error'; content: string },
void,
unknown
>;
Expand Down
Loading
Loading