Skip to content

Commit 38befac

Browse files
committed
feat(webui): route session deletion through the engine deleteSession facade
1 parent 25da27a commit 38befac

32 files changed

Lines changed: 1864 additions & 870 deletions

‎packages/webui/docs/ARCHITECTURE.md‎

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
│ server/lib/ — pure modules (one concern each) │
7575
│ │
7676
│ config · layout · lan · models · sqlite-resolver · │
77-
│ mcode-session-delete · sessions · state-bus · acp-client │
77+
│ sessions · state-bus · acp-client │
7878
│ mcode-rpc · mcode-acp · mcode-exec · chat-line · context-percent │
7979
│ mavis-usage · usage · settings · upload · workspace · slash · │
8080
│ static · gates · auth · alerts · trajectory │
@@ -312,9 +312,11 @@ flowchart TD
312312
Lifecycle notes:
313313

314314
- **Delete** (`DELETE /api/sessions/:id`) removes the webui record AND
315-
cross-deletes the linked `mvs_…` rows from the runtime SQLite
316-
(`deleteMcodeSessionFromDb`, `?dryRun=true` to preview). Deleting the
317-
mcode record drops it from both lists in one transaction.
315+
asks the engine to delete the linked `mvs_…` session
316+
(`engine/session-delete.js#deleteSessionThroughEngine`, `?dryRun=true`
317+
to preview — the preview is a read-only per-table count, because the
318+
engine's delete has no preview form). Deleting the mcode record drops
319+
it from both lists.
318320
- **Startup cleanup** prunes records that are empty AND default-titled
319321
AND older than 24h — the "+"-then-never-typed leftovers.
320322
- **Search** (`GET /api/sessions/search`) fuzzy-matches titles across
@@ -509,7 +511,7 @@ Fifteen files, one job each:
509511
| `engine/account-reads.js` | The account family's facade call (`readEngineAccount`) and the endpoint→capability table `ACCOUNT_READ_ENDPOINTS` (step M3, batch B4). Gates **hard** on `authCredentials` · `getAccountStatus` — the same pair and the same provider method as `engine/usage-reads.js`, because #20 and #15/#16 read the same engine projection. Its read is **asynchronous** and it lives under the ordinary `await import()` boot-path rule |
510512
| `engine/model-reads.js` | The model-catalogue family's facade call (`readEngineModelCatalogue`), the whole projection as named pure functions (`projectModelCatalogue`, `deriveModelSelection`, `buildModelCataloguePayload`, `catalogueSourceLabel`, `webuiFullModelId`, `providerOfModelId`, `attachContextWindowOptions`, `configOption`), and the endpoint→capability table `MODEL_READ_ENDPOINTS` (step M3, batch B4). Gates **soft**: `checkModelReadCapability` reports and never throws, because the catalogue's primary sources are files webui owns. Its read is **synchronous**, and it is the one engine module **not** re-exported from `engine/index.js` — see the boot-path note below |
511513
| `engine/capability-reads.js` | The capability-declaration family's facade call (`readEngineCapabilityView`) and the endpoint→capability table `CAPABILITY_READ_ENDPOINTS` (step M3, batch B4). Declares **no capability for #73** — it IS the declaration endpoint, and gating the gate would let a `none` hide the declaration that says so. It is the only endpoint in the migration whose response CONTRACT changed (`capabilities` is now the 14-key declaration, replacing the ACP wire table) |
512-
| `engine/session-writes.js` | The session WRITE family's facade calls (`planEngineSessionDelete`, `commitEngineSessionDelete`, `commitEngineOrphanSessionDelete`, `previewEngineSessionDelete`, `applyEngineSessionRename`, `readOrphanSessionWriteIds`), the pure derivations they are built from (`resolveSessionTarget`, `isMcodeSessionId`, `isOrphanSessionRecord`, `selectOrphanSessionIds`, the two fan-out predicates, the per-client state resets), and the endpoint→capability table `SESSION_WRITE_ENDPOINTS` (step M3, batch B5). Gates **hard** on `sessionCrud` · `deleteSession` for #7 and #6, and declares **no capability at all** for #4. Forwards the 32-table SQL to `lib/mcode-session-delete.js` rather than moving it — see the write-path section below |
514+
| `engine/session-writes.js` | The session WRITE family's facade calls (`planEngineSessionDelete`, `commitEngineSessionDelete`, `commitEngineOrphanSessionDelete`, `previewEngineSessionDelete`, `applyEngineSessionRename`, `readOrphanSessionWriteIds`), the pure derivations they are built from (`resolveSessionTarget`, `isMcodeSessionId`, `isOrphanSessionRecord`, `selectOrphanSessionIds`, the two fan-out predicates, the per-client state resets), and the endpoint→capability table `SESSION_WRITE_ENDPOINTS` (step M3, batch B5). Gates **hard** on `sessionCrud` · `deleteSession` for #7 and #6, and declares **no capability at all** for #4. Issues no SQL of its own — the engine destroys the rows, through `engine/session-delete.js` — see the write-path section below |
513515

514516
Routes take the host from the facade and never from `lib/acp-client.js`:
515517
`routes/plugins.js` and `routes/turn-diff.js` call
@@ -701,7 +703,7 @@ same rule: their static imports are `engine/capabilities.js` and
701703
`await import()` inside the functions. `engine/session-writes.js` adds
702704
`node:fs` at module scope (a builtin, and `engine/usage-reads.js`
703705
already does the same) and reaches `lib/sessions.js`,
704-
`lib/mcode-session-delete.js`, `lib/state-bus.js` and
706+
`engine/session-delete.js`, `lib/state-bus.js` and
705707
`lib/config.js` dynamically — all six of its storage dependencies, which
706708
is what lets it be re-exported from `engine/index.js` at all.
707709

@@ -1119,7 +1121,7 @@ answer twice in a row.
11191121

11201122
| Endpoint | Facade function | Capability · sub-item | Enforcement | Value source |
11211123
| --- | --- | --- | --- | --- |
1122-
| `DELETE /api/sessions/:id` (#7) | `engine/session-writes.js#planEngineSessionDelete` → `engine/session-writes.js#commitEngineSessionDelete` / `engine/session-writes.js#commitEngineOrphanSessionDelete` / `engine/session-writes.js#previewEngineSessionDelete` | `sessionCrud` · `deleteSession` | hard — 501 | the webui session store, the in-memory ACP session cache, the sidebar tree cache, and the engine's own `local_runtime_*` rows via `lib/mcode-session-delete.js#deleteMcodeSessionFromDb` |
1124+
| `DELETE /api/sessions/:id` (#7) | `engine/session-writes.js#planEngineSessionDelete` → `engine/session-writes.js#commitEngineSessionDelete` / `engine/session-writes.js#commitEngineOrphanSessionDelete` / `engine/session-writes.js#previewEngineSessionDelete` | `sessionCrud` · `deleteSession` | hard — 501 | the webui session store, the in-memory ACP session cache, the sidebar tree cache, and the engine's own `local_runtime_*` rows via the engine's `deleteSession`, reached through `engine/session-delete.js#deleteSessionThroughEngine` |
11231125
| `POST /api/sessions/rename` (#4) | `engine/session-writes.js#applyEngineSessionRename` | none of the 14 keys | none — the gate is a reported no-op | webui's own session store, and nothing else. The engine's title is not written |
11241126
| `POST /api/sessions/cleanup-orphans` (#6) | `engine/session-writes.js#readOrphanSessionWriteIds`, then each selected id delegated to `engine/session-writes.js#commitEngineOrphanSessionDelete` | `sessionCrud` · `deleteSession` | hard — 501 | the same store, plus each selected id delegated to #7, so it reaches the same engine rows |
11251127

@@ -1191,16 +1193,32 @@ memory and rewrites its registry row on its next request — that is the
11911193
cache: invalidating the whole cache empties the sidebar, refills it, and
11921194
reads to the user like the delete failed.
11931195

1194-
**The 32-table SQL was not moved, and that is recorded rather than
1195-
quietly dropped.** The plan for this batch annotated
1196-
`lib/mcode-session-delete.js` "delete". It is kept because
1197-
`lib/acp-client.js` imports `deleteMcodeSessionFromDb` from it and four
1198-
test files bind to that specifier; collecting it means moving those
1199-
first. The facade reaches it through `await import()` and issues no SQL
1200-
of its own — the same split B3 drew for `lib/mavis-usage.js` and B4 for
1201-
`lib/mcode-rpc.js`. A test asserts both halves: the table list is still
1202-
32 entries exported from the lib module, and the facade contains no SQL
1203-
verb at all.
1196+
**M4-3a collected the debt this batch recorded: the 32-table
1197+
row-destroying SQL is gone, and the engine destroys the rows.** The
1198+
retired module (the bare-SQL session delete, removed here) opened the
1199+
engine's runtime database and deleted by hand across a hand-curated
1200+
table list; the
1201+
destructive step is now the engine's own `deleteSession`, reached from
1202+
`engine/session-delete.js` through the process-local catalogue host — the
1203+
same `getEngineCatalogueHost()` seam the plugin and turn-diff routes use.
1204+
The facade reaches that module through `await import()` and still issues
1205+
no SQL of its own.
1206+
1207+
**What stayed is the read-only half, and the reason is a fact rather than
1208+
caution: the engine exposes no dry-run or preview form of its delete.**
1209+
`?dryRun=true` is part of #7 and #6's contract, so the per-table COUNT
1210+
remains, against the same 32-table list, in the same module that now
1211+
drives the engine call. webui still READS a schema layout it maintains by
1212+
hand; it no longer WRITES one. The real delete takes its `log` and
1213+
`totalRowsDeleted` from the same readonly pass, taken immediately before
1214+
the engine call, so the `rowsAffected` and `mcodeRowsAffected` fields the
1215+
HTTP layer derives from them carry the values they always carried.
1216+
1217+
`test/lib/engine/session-delete-ownership.test.js` is the tripwire: no
1218+
server module may import the retired module, issue a DELETE against a
1219+
`local_runtime_*` table, or open the engine's database without
1220+
`readonly: true`. It is a static-source check on purpose — a behavioural
1221+
test cannot distinguish "the engine deleted it" from "webui deleted it".
12041222

12051223
**#6's response shape is this batch's byte-for-byte red line, so the
12061224
payload is built in the facade and never re-assembled in the route.** The
@@ -1216,12 +1234,15 @@ turn a cleanup request into a 500. `dryRun` suppresses the kill and the
12161234
cache drop, because a preview mutates nothing and a preview that shuts
12171235
down the user's ACP child is a side effect the `?dryRun=true` contract
12181236
does not include; the COUNT still runs, read-only, inside
1219-
`lib/mcode-session-delete.js`.
1237+
`engine/session-delete.js`.
12201238

12211239
**Three things this batch records as known debt instead of deciding:**
12221240

1223-
1. The 32-table SQL is still in `lib/mcode-session-delete.js`, for the
1224-
consumer reasons above.
1241+
1. Collected in M4-3a: the 32-table row-destroying SQL. What remains of
1242+
the same debt is the READ side — `engine/session-delete.js` still
1243+
counts against a hand-maintained table list, because the engine has no
1244+
preview form of its delete. Closing that needs a count surface on the
1245+
engine itself, which is a local-runtime-v2 change.
12251246
2. A rename is a **webui-side label only**. The engine's own title in
12261247
`local_runtime_sessions` is untouched while the sidebar tree reads its
12271248
titles from the engine, so for an engine-backed session a rename can be

‎packages/webui/docs/ARCHITECTURE.zh-CN.md‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
│ server/lib/ — pure modules (one concern each) │
7070
│ │
7171
│ config · layout · lan · models · sqlite-resolver · │
72-
│ mcode-session-delete · sessions · state-bus · acp-client │
72+
│ sessions · state-bus · acp-client │
7373
│ mcode-rpc · mcode-acp · mcode-exec · chat-line · context-percent │
7474
│ mavis-usage · usage · settings · upload · workspace · slash · │
7575
│ static · gates · auth · alerts · trajectory │
@@ -304,9 +304,10 @@ flowchart TD
304304
生命周期说明:
305305

306306
- **删除**(`DELETE /api/sessions/:id`)会移除 webui 记录,并
307-
从运行时 SQLite 中交叉删除关联的 `mvs_…` 行
308-
(`deleteMcodeSessionFromDb`,用 `?dryRun=true` 预览)。删除
309-
mcode 记录会在一个事务中把它从两个列表里都移除。
307+
请求引擎删除关联的 `mvs_…` 会话
308+
(`engine/session-delete.js#deleteSessionThroughEngine`,用 `?dryRun=true`
309+
预览——预览是逐表只读计数,因为引擎的删除没有预览形态)。删除
310+
mcode 记录会把它从两个列表里都移除。
310311
- **启动清理**会剔除那些为空、且仍是默认标题、
311312
且超过 24 小时的记录——即点了「+」却从未输入的残留。
312313
- **搜索**(`GET /api/sessions/search`)跨工作区对标题做
@@ -994,7 +995,7 @@ webui 将每个事件视为幂等更新;重放同一
994995

995996
| 端点 | 门面函数 | 能力 · 子项 | 强制方式 | 取值来源 |
996997
| --- | --- | --- | --- | --- |
997-
| `DELETE /api/sessions/:id`(#7) | `engine/session-writes.js#planEngineSessionDelete` → `engine/session-writes.js#commitEngineSessionDelete` / `engine/session-writes.js#commitEngineOrphanSessionDelete` / `engine/session-writes.js#previewEngineSessionDelete` | `sessionCrud` · `deleteSession` | 硬——501 | webui 的会话存储、内存中的 ACP 会话缓存、侧栏树缓存,以及经 `lib/mcode-session-delete.js#deleteMcodeSessionFromDb` 触达的引擎自己的 `local_runtime_*` 行 |
998+
| `DELETE /api/sessions/:id`(#7) | `engine/session-writes.js#planEngineSessionDelete` → `engine/session-writes.js#commitEngineSessionDelete` / `engine/session-writes.js#commitEngineOrphanSessionDelete` / `engine/session-writes.js#previewEngineSessionDelete` | `sessionCrud` · `deleteSession` | 硬——501 | webui 的会话存储、内存中的 ACP 会话缓存、侧栏树缓存,以及经 `engine/session-delete.js#deleteSessionThroughEngine` 触达的、由引擎自己的 `deleteSession` 移除的 `local_runtime_*` 行 |
998999
| `POST /api/sessions/rename`(#4) | `engine/session-writes.js#applyEngineSessionRename` | 14 个键里的任何一个都不适用 | 不门控——门控是「被报告的空操作」 | 只有 webui 自己的会话存储。引擎的标题**不**被写入 |
9991000
| `POST /api/sessions/cleanup-orphans`(#6) | `engine/session-writes.js#readOrphanSessionWriteIds`,随后逐个委派给 `engine/session-writes.js#commitEngineOrphanSessionDelete` | `sessionCrud` · `deleteSession` | 硬——501 | 同一份存储,加上每个被选中的 id 都走 #7 的真实删除分支,因此抵达同一批引擎行 |
10001001

@@ -1048,12 +1049,23 @@ ACP 子进程在行被移除**之前**被停掉,因为它在内存里持有那
10481049
请求里重写自己的注册行——那就是「已删除的会话又冒出来」这个 bug。离开缓存的只有
10491050
**那一个**被删的 sid:把整份缓存作废会清空侧栏、再把它填满,读到用户那里就像删除失败。
10501051

1051-
**32 张表的 SQL 没有被搬走,这是被记录下来而不是被悄悄丢掉的。** 本批的批次计划给
1052-
`lib/mcode-session-delete.js` 批注了「delete」。它被保留,是因为
1053-
`lib/acp-client.js` 从它那里导入,而有四个测试文件绑定在那个导出名上;收集它意味着
1054-
先搬走那些。门面通过 `await import()` 触达它,自己不发任何 SQL——这正是 B3 为
1055-
`lib/mavis-usage.js`、B4 为 `lib/mcode-rpc.js` 划下的同一条分界。一个测试同时断言
1056-
两半:表清单仍然是从那个 lib 模块导出的 32 条,而门面里一个 SQL 动词都没有。
1052+
**M4-3a 收集了本批记录下来的那笔债:32 张表的删行 SQL 已经退场,行由引擎删除。**
1053+
被退役的模块(裸 SQL 会话删除)曾打开引擎的运行时数据库,在一份手工维护的表清单上
1054+
逐表删除;那个破坏性步骤现在是引擎自己的 `deleteSession`,经由进程内的 catalogue host
1055+
从 `engine/session-delete.js` 触达——也就是插件路由与 turn-diff 路由所用的同一个
1056+
`getEngineCatalogueHost()` 接缝。门面通过 `await import()` 触达那个模块,自己仍然不发
1057+
任何 SQL。
1058+
1059+
**留下的是只读的那一半,理由是事实而不是谨慎:引擎的删除没有 dry-run 或预览形态。**
1060+
`?dryRun=true` 是 #7 与 #6 契约的一部分,因此逐表 COUNT 仍然存在,仍然对着同一份
1061+
32 表清单,并且就住在现在驱动引擎调用的那个模块里。webui 仍然**读**一份自己手工维护
1062+
的 schema 布局;它不再**写**这一份。真实删除的 `log` 与 `totalRowsDeleted` 取自紧邻
1063+
引擎调用之前的那次只读统计,因此 HTTP 层由它们派生的 `rowsAffected` 与
1064+
`mcodeRowsAffected` 字段携带的仍是它们一直携带的数值。
1065+
1066+
`test/lib/engine/session-delete-ownership.test.js` 是那道红线:任何 server 模块都不得
1067+
导入被退役的模块、对 `local_runtime_*` 表发 DELETE、或以非 `readonly: true` 的方式
1068+
打开引擎的数据库。它刻意是静态源码检查——行为测试无法区分「引擎删的」与「webui 删的」。
10571069

10581070
**#6 的响应形状是本批逐字节的红线,因此载荷在门面里组装、绝不在路由里重装。**
10591071
预览是四个键、且就是这个顺序的 `{ok, dryRun, count, ids}`;而真实路径的空操作是
@@ -1063,11 +1075,13 @@ ACP 子进程在行被移除**之前**被停掉,因为它在内存里持有那
10631075
(由编辑器而非 webui 写入)并被原样保留;解析失败回答 `[]`——门面前代码也是这么做的,
10641076
而一份损坏的存储不得把一次清理请求变成 500。`dryRun` 会抑制子进程 kill 与缓存丢弃,
10651077
因为一次预览不改写任何东西,而一次关掉用户 ACP 子进程的预览是 `?dryRun=true` 契约
1066-
并不包含的副作用;那条 COUNT 仍会跑,只读地跑在 `lib/mcode-session-delete.js` 里。
1078+
并不包含的副作用;那条 COUNT 仍会跑,只读地跑在 `engine/session-delete.js` 里。
10671079

10681080
**本批记为已知债而不予决定的三件事:**
10691081

1070-
1. 32 张表的 SQL 仍然在 `lib/mcode-session-delete.js` 里,理由见上文那些消费方。
1082+
1. M4-3a 已收集:32 张表的删行 SQL。同一笔债剩下的是**读**的一侧——
1083+
`engine/session-delete.js` 仍在对着手工维护的表清单做统计,因为引擎的删除没有预览
1084+
形态。关掉它需要在引擎自身上加一个计数接口,那是 local-runtime-v2 的改动。
10711085
2. 改名只是**一个 webui 侧的标签**。`local_runtime_sessions` 里引擎自己的标题没有被
10721086
触碰,而侧栏树是从引擎读标题的。因此对一个由引擎支撑的会话,一次改名可能在包装
10731087
列表里看得见、在树里看不见。这是既有行为,本批没有改动它;关掉它意味着决定哪一份

0 commit comments

Comments
 (0)