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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ lives under a budget, and what it is doing is visible (plan 2026-10-02, `C-7…C
`package.nls*`, the runtime words through `l10n/` bundles, with key-parity tests holding the two
together; the server's logs, CLI and prompts stay English. A `bundlesDirectory` field joins
`cxxModules/cache`'s `paths`, so a client never guesses where a diagnostic bundle lands.
- **Hardened by the pre-release review.** A 0.0.9 instance working beside a 0.0.10 one keeps its
cache: the lease tick now gives an undescribed instance directory the same 24-hour grace the
sweep does, and a reset no longer deletes the resetting instance's own `instance.json` (the
heartbeat that says it is alive). The sweep command and a stale `cxxModules/cache` walk the
tree off the event loop — the reply arrives when the work is done, and no request or lease
renewal waits behind minutes of filesystem work; every sweep, background or interactive, is one
at a time with later ones coalesced. An unreadable process identity (another user's process on
Windows, a failed `ps` on macOS) no longer reads as a dead lease owner — only a definite answer
releases a lease early. A crashed clangd no longer pins its generation's start forever, so
`staleCommands` sweeps are possible again after a crash. Clock steps back no longer reap every
live instance. Budget settings that do not parse are rejected whole (`1.5G` is not `1G`) and
logged; deletions count what they could not take instead of what they attempted; a dry run
changes nothing the server remembers; a multi-root sweep answers with the sum over its roots.

## [0.0.9] — 2026-10-02

Expand Down
46 changes: 44 additions & 2 deletions conformance/traceability.json
Original file line number Diff line number Diff line change
Expand Up @@ -2083,7 +2083,7 @@
],
"S3-5.8-2": [
{
"manual": "the sweep answers from the session loop after the removal, the way mcppls.resetCache already did; it cancels nothing and fails no request an engine owed (review of src/server/session.cpp and src/orchestrator/workspace.cpp)"
"manual": "the sweep's removals run on a thread of their own and the reply returns to the session loop as an event (review of src/server/session.cpp, mcppls.sweepCache branch); it cancels nothing and fails no request an engine owed"
}
],
"S3-5.8-3": [
Expand All @@ -2110,7 +2110,11 @@
"S3-5.8-7": [
{
"script": "src/orchestrator/workspace.cpp",
"contains": "impl.sweepRunning_.exchange(true)"
"contains": "sweepPending_"
},
{
"script": "src/orchestrator/workspace.cpp",
"contains": "impl.sweepRunning_.store(true)"
}
],
"S3-5.8-8": [
Expand All @@ -2120,5 +2124,43 @@
{
"check": "cache-budget/sweep-dry-run"
}
],
"S3-5.7-8": [
{
"script": "src/orchestrator/workspace.cpp",
"contains": "start_cache_task_(\"report\""
}
],
"S3-5.8-9": [
{
"script": "src/orchestrator/workspace.cpp",
"contains": "failed += removed.failed"
},
{
"script": "src/cli/cache.cpp",
"contains": "failed += removed.failed"
}
],
"S3-5.8-10": [
{
"script": "src/server/session.cpp",
"contains": "run_prepared_sweep"
},
{
"script": "src/orchestrator/workspace.cppm",
"contains": "deferred_answer"
}
],
"S3-5.8-11": [
{
"script": "src/server/session.cpp",
"contains": "freed += outcome.value(\"bytes\""
}
],
"S3-5.8-12": [
{
"script": "src/orchestrator/workspace.cpp",
"contains": "no numbers are remembered"
}
]
}
2 changes: 1 addition & 1 deletion docs/30-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ either wrapped in a top-level `mcppls` object or not.
| `mcppls.mcpp` | a path | *(empty)* | `--mcpp` | reload | The `mcpp` executable for mcpp projects; empty means found on `PATH`. |
| `mcppls.payload` | a path | *(empty)* | `--payload` | restart | Payload directory with clangd and the semantic kit; overridden per-file by `clangd` and `kit` below. |
| `mcppls.clangd` | a path | *(empty)* | `--clangd` | restart | clangd executable, overriding the one the payload carries. |
| `mcppls.cache.maxBytes` | ? | ? | `--cache-max-bytes` | restart | How large one workspace's module cache may get. Copies and dead instance directories are removed to stay under it; the published BMIs never are, so a cache that cannot get under the limit without them is reported instead (the status bar and the cache menu say so). `unlimited` turns the budget off. |
| `mcppls.cache.maxBytes` | ? | ? | `--cache-max-bytes` | restart | How large one workspace's module cache may get. Reaching it is reported -- the status bar and the cache menu say near or over; keeping under it is the sweeps' own work (copies and dead instance directories go, published BMIs never), and all workspaces together answer to cache.totalBytes. A cache that cannot get under the limit without a published BMI is only reported. `unlimited` turns the budget off, `0` keeps none of what a sweep may remove. |
| `mcppls.cache.totalBytes` | ? | ? | `--cache-total-bytes` | restart | How large all workspaces' module caches may get together. Only workspaces no instance has open give anything up, oldest-used first; published BMIs are never removed. |
| `mcppls.cache.instanceGrace` | a non-negative number of seconds | `86400` | `--cache-instance-grace` | restart | How long an instance directory that says nothing about itself (a leftover of mcppls 0.0.9 or older) is kept before it is removed: 86400, the default, is 24 hours. Directories that do describe themselves are judged by their own heartbeat instead. |
| `mcppls.cache.showInStatusBar` | `auto`, `always`, `never` | `auto` | — | immediately | Whether the status bar shows the cache size. `auto` shows it only when the cache is near or over its budget; `always` and `never` do what they say. The hover card and the menu answer for the rest either way. |
Expand Down
9 changes: 5 additions & 4 deletions docs/specs/s3-lsp-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ interface CacheInstanceInfo {
}
```

A server that declared `cxxModules` **MUST** answer `cxxModules/cache` for every root it serves with the numbers of the cache it actually holds. <a id="S3-5.7-1"></a><sup>S3-5.7-1</sup> A server **MUST NOT** remove, move or rewrite anything as a result of the request: it is a read. <a id="S3-5.7-2"></a><sup>S3-5.7-2</sup> A server **MAY** answer from a report it cached for at most 30 seconds, and **MUST** recompute that report before answering when a sweep of the same root finished after the cached one was made, so what a client shows after a sweep is what the sweep left. <a id="S3-5.7-3"></a><sup>S3-5.7-3</sup> The `prompts` the report carries are rendered by the server itself, for a person to hand to a local agent; they name the read-only commands to look at and the paths on this machine, and the server **MUST NOT** send them, or any other part of the report, anywhere. <a id="S3-5.7-4"></a><sup>S3-5.7-4</sup> A client **MUST** treat every path and name in the report as text: it renders them escaped, and never turns a server-sent string into a command, a URL or markup of its own. <a id="S3-5.7-5"></a><sup>S3-5.7-5</sup> A server that does not know the request answers `MethodNotFound`, and a client that receives it falls back to the status's `cache` field or to the CLI. <a id="S3-5.7-6"></a><sup>S3-5.7-6</sup> `paths` names the three places a person investigating the cache is sent to: the root's own module cache (`cacheRoot`), the server's logs (`logDirectory`), and where diagnostic bundles are written (`bundlesDirectory`, the default the bundle writer uses); a client that reveals a directory reveals one of these, and nothing it guesses itself. <a id="S3-5.7-7"></a><sup>S3-5.7-7</sup> The agent prompt is a task book, not a transcript: the verified facts, the read-only checks each with what healthy looks like, the output contract (a verdict, the evidence, what could be done without deleting), and a bug branch that asks the developer first and only then -- with their agreement -- drafts the issue, shows the draft for approval, and names the bundle paths for the person to attach; the prompt **MUST** state that the agent never uploads logs or bundles itself.
A server that declared `cxxModules` **MUST** answer `cxxModules/cache` for every root it serves with the numbers of the cache it actually holds. <a id="S3-5.7-1"></a><sup>S3-5.7-1</sup> A server **MUST NOT** remove, move or rewrite anything as a result of the request: it is a read. <a id="S3-5.7-2"></a><sup>S3-5.7-2</sup> A server **MAY** answer from a report it cached for at most 30 seconds, and **MUST** recompute that report before answering when a sweep of the same root finished after the cached one was made, so what a client shows after a sweep is what the sweep left. <a id="S3-5.7-3"></a><sup>S3-5.7-3</sup> The `prompts` the report carries are rendered by the server itself, for a person to hand to a local agent; they name the read-only commands to look at and the paths on this machine, and the server **MUST NOT** send them, or any other part of the report, anywhere. <a id="S3-5.7-4"></a><sup>S3-5.7-4</sup> A client **MUST** treat every path and name in the report as text: it renders them escaped, and never turns a server-sent string into a command, a URL or markup of its own. <a id="S3-5.7-5"></a><sup>S3-5.7-5</sup> A server that does not know the request answers `MethodNotFound`, and a client that receives it falls back to the status's `cache` field or to the CLI. <a id="S3-5.7-6"></a><sup>S3-5.7-6</sup> `paths` names the three places a person investigating the cache is sent to: the root's own module cache (`cacheRoot`), the server's logs (`logDirectory`), and where diagnostic bundles are written (`bundlesDirectory`, the default the bundle writer uses); a client that reveals a directory reveals one of these, and nothing it guesses itself. <a id="S3-5.7-7"></a><sup>S3-5.7-7</sup> The agent prompt is a task book, not a transcript: the verified facts, the read-only checks each with what healthy looks like, the output contract (a verdict, the evidence, what could be done without deleting), and a bug branch that asks the developer first and only then -- with their agreement -- drafts the issue, shows the draft for approval, and names the bundle paths for the person to attach; the prompt **MUST** state that the agent never uploads logs or bundles itself. <a id="S3-5.7-8"></a><sup>S3-5.7-8</sup> A server **MUST NOT** hold the loop that serves requests to recompute a stale report: walking a grown cache is filesystem work, and it runs off that loop -- the request is answered from the numbers already computed, and the refreshed numbers arrive with the next answer.

### 5.8 `mcppls.sweepCache`

Expand All @@ -331,15 +331,16 @@ interface SweepCacheParams {
interface SweepCacheResult {
ok: true;
freedBytes: number; // what the sweep freed, or would have under `dryRun`
files: number; // the copies and command directories counted in `freedBytes`
files: number; // the copies counted in `freedBytes`
instances: number; // the instance directories removed
failed: number; // entries a removal could not take (a lock, a scanner holding the file)
roots: number; // how many roots were swept
dryRun: boolean;
alreadyRunning?: boolean; // a sweep was in flight; nothing was done by this one
alreadyRunning?: boolean; // every swept root had a sweep in flight; nothing was done by this one
}
```

A server that advertises the command **MUST NOT** stop, restart or interrupt any engine for a sweep's sake. <a id="S3-5.8-1"></a><sup>S3-5.8-1</sup> A sweep **MUST NOT** let a request any engine owed fail. <a id="S3-5.8-2"></a><sup>S3-5.8-2</sup> A server **MUST NOT** remove a published BMI -- a `<module>.pcm` whose name is not the versioned copy shape. <a id="S3-5.8-3"></a><sup>S3-5.8-3</sup> A server **MUST NOT** remove a file a live engine generation could hold mapped: a copy is swept only when its mtime is older than the start of the oldest live generation of the engines using that cache, or when no engine uses the cache at all. <a id="S3-5.8-4"></a><sup>S3-5.8-4</sup> A server **MUST NOT** sweep a workspace's cache that another instance has open: an instance directory whose own heartbeat is fresh belongs to a live instance, whatever the workspace's lease says. <a id="S3-5.8-5"></a><sup>S3-5.8-5</sup> The `staleCommands` category -- the command directories older units' BMIs occupy -- is the engine start path's own work (C-2), so a server **MUST** skip it unless the client asked for it by name and no engine is live in that root. <a id="S3-5.8-6"></a><sup>S3-5.8-6</sup> A server **MUST** run one sweep at a time, and answer a sweep that arrives while one runs with `alreadyRunning: true` and nothing removed by it. <a id="S3-5.8-7"></a><sup>S3-5.8-7</sup> With `dryRun: true` a server **MUST** compute the answer over exactly the set it would have removed, so what a client reports as "would free" is what a sweep would free. <a id="S3-5.8-8"></a><sup>S3-5.8-8</sup>
A server that advertises the command **MUST NOT** stop, restart or interrupt any engine for a sweep's sake. <a id="S3-5.8-1"></a><sup>S3-5.8-1</sup> A sweep **MUST NOT** let a request any engine owed fail. <a id="S3-5.8-2"></a><sup>S3-5.8-2</sup> A server **MUST NOT** remove a published BMI -- a `<module>.pcm` whose name is not the versioned copy shape. <a id="S3-5.8-3"></a><sup>S3-5.8-3</sup> A server **MUST NOT** remove a file a live engine generation could hold mapped: a copy is swept only when its mtime is older than the start of the oldest live generation of the engines using that cache, or when no engine uses the cache at all. <a id="S3-5.8-4"></a><sup>S3-5.8-4</sup> A server **MUST NOT** sweep a workspace's cache that another instance has open: an instance directory whose own heartbeat is fresh belongs to a live instance, whatever the workspace's lease says. <a id="S3-5.8-5"></a><sup>S3-5.8-5</sup> The `staleCommands` category -- the command directories older units' BMIs occupy -- is the engine start path's own work (C-2), so a server **MUST** skip it unless the client asked for it by name and no engine is live in that root. <a id="S3-5.8-6"></a><sup>S3-5.8-6</sup> A server **MUST** run one sweep at a time, and answer a sweep that arrives while one runs with `alreadyRunning: true` and nothing removed by it. <a id="S3-5.8-7"></a><sup>S3-5.8-7</sup> With `dryRun: true` a server **MUST** compute the answer over exactly the set it would have removed, so what a client reports as "would free" is what a sweep would free. <a id="S3-5.8-8"></a><sup>S3-5.8-8</sup> What a sweep counts freed is what left the disk: an entry a removal could not take is counted in `failed`, never in `freedBytes`. <a id="S3-5.8-9"></a><sup>S3-5.8-9</sup> A server **MUST NOT** run the removals on the loop that serves requests and renews leases: the sweep of a grown cache is minutes of filesystem work, the reply arrives when the work is done, and nothing a client asked before it -- the lease's own renewal included -- waits behind it. <a id="S3-5.8-10"></a><sup>S3-5.8-10</sup> With more than one root swept, the numbers are the sum over those roots and `roots` says how many there were; `alreadyRunning` appears only when every swept root had a sweep in flight. <a id="S3-5.8-11"></a><sup>S3-5.8-11</sup> A `dryRun` sweep also changes nothing the server remembers: no `lastSweep`, no recomputed report -- a preview a client showed must not later read as a sweep that happened. <a id="S3-5.8-12"></a><sup>S3-5.8-12</sup>

## 6. Module features through standard LSP

Expand Down
2 changes: 1 addition & 1 deletion docs/zh-CN/30-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ VS Code 扩展已经会这样做);`重新加载模型` 只重新加载项目
| `mcppls.mcpp` | 路径 | (空) | `--mcpp` | 重新加载模型 | mcpp 项目所用的 `mcpp` 可执行文件;空表示在 `PATH` 上查找。 |
| `mcppls.payload` | 路径 | (空) | `--payload` | 重启 | 包含 clangd 和语义工具包的 payload 目录;下面的 `clangd` 和 `kit` 可以分别覆盖其中一项。 |
| `mcppls.clangd` | 路径 | (空) | `--clangd` | 重启 | clangd 可执行文件,覆盖 payload 自带的那一份。 |
| `mcppls.cache.maxBytes` | ? | ? | `--cache-max-bytes` | 重启 | 单个工作区的模块缓存上限。超出时先清理副本与死实例目录回到预算内;已发布的模块本体(BMI)永远不会被删——删净副本仍超限时只报告(状态栏与缓存菜单可见)。`unlimited` 关闭预算。 |
| `mcppls.cache.maxBytes` | ? | ? | `--cache-max-bytes` | 重启 | 单个工作区的模块缓存上限。接近或超出时会报告——状态栏与缓存菜单显示 near/over;回到预算内是自动清扫的事(删副本与死实例目录,已发布的 BMI 永不删除),所有工作区加在一起的上限由 cache.totalBytes 负责。删净可删仍超限时只报告不硬删。`unlimited` 关闭预算,`0` 表示可清理的一律不留。 |
| `mcppls.cache.totalBytes` | ? | ? | `--cache-total-bytes` | 重启 | 所有工作区模块缓存的总上限。只有没有实例打开的工作区按最久未用的先后让出副本;已发布的模块本体不会被删。 |
| `mcppls.cache.instanceGrace` | 非负整数(秒) | `86400` | `--cache-instance-grace` | 重启 | 一个不自述的实例目录(0.0.9 及更早版本的遗留)在删除前保留多久:默认 86400 秒,即 24 小时。会自述的目录按它自己的心跳判断。 |
| `mcppls.cache.showInStatusBar` | `auto`, `always`, `never` | `auto` | — | 立即生效 | 状态栏是否显示缓存大小。`auto` 只在缓存接近或超过预算时显示;`always` 与 `never` 如字面。无论如何,其余数字看悬停卡片与菜单。 |
Expand Down
4 changes: 2 additions & 2 deletions editors/vscode/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"{0} modules · {1} units": "{0} modules · {1} units",
"{0} s ago": "{0} s ago",
"A sweep is already running.": "A sweep is already running.",
"A sweep would free {0} bytes ({1} files). Nothing was removed.": "A sweep would free {0} bytes ({1} files). Nothing was removed.",
"A sweep would free {0} ({1} files). Nothing was removed.": "A sweep would free {0} ({1} files). Nothing was removed.",
"A sweep would free nothing: there is nothing to remove.": "A sweep would free nothing: there is nothing to remove.",
"Back": "Back",
"Cache in use": "Cache in use",
Expand All @@ -33,7 +33,7 @@
"Error": "Error",
"Feedback": "Feedback",
"freed {0} ({1} files) · {2} ago": "freed {0} ({1} files) · {2} ago",
"Freed {0} bytes ({1} files). No restart, no rebuild.": "Freed {0} bytes ({1} files). No restart, no rebuild.",
"Freed {0} ({1} files). No restart, no rebuild.": "Freed {0} ({1} files). No restart, no rebuild.",
"Instances": "Instances",
"Largest modules": "Largest modules",
"Last sweep": "Last sweep",
Expand Down
4 changes: 2 additions & 2 deletions editors/vscode/l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"{0} modules · {1} units": "{0} 个模块 · {1} 个单元",
"{0} s ago": "{0} 秒前",
"A sweep is already running.": "已有一次清理正在进行。",
"A sweep would free {0} bytes ({1} files). Nothing was removed.": "预演:可释放 {0} 字节({1} 个文件)。未删除任何东西。",
"A sweep would free {0} ({1} files). Nothing was removed.": "预演:可释放 {0}({1} 个文件)。未删除任何东西。",
"A sweep would free nothing: there is nothing to remove.": "预演:无可释放的内容,没有要删除的。",
"Back": "返回",
"Cache in use": "缓存占用",
Expand All @@ -33,7 +33,7 @@
"Error": "错误",
"Feedback": "反馈",
"freed {0} ({1} files) · {2} ago": "释放 {0}({1} 个文件)· {2}",
"Freed {0} bytes ({1} files). No restart, no rebuild.": "已释放 {0} 字节({1} 个文件)。不重启、不重编。",
"Freed {0} ({1} files). No restart, no rebuild.": "已释放 {0}({1} 个文件)。不重启、不重编。",
"Instances": "实例目录",
"Largest modules": "最大模块",
"Last sweep": "上次清理",
Expand Down
Loading
Loading