Skip to content

fix(library): 自定义媒体库暂时不可用时不重建空库 - #4728

Merged
MagicLizi merged 19 commits into
makecindy:mainfrom
PraiseZhu:feat/mivo-library-root-recovery-20260920
Sep 24, 2026
Merged

MagicLizi merged 19 commits into
makecindy:mainfrom
PraiseZhu:feat/mivo-library-root-recovery-20260920

Conversation

@PraiseZhu

@PraiseZhu PraiseZhu commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

这次改了什么

摘要

自定义 Library 已打开后,如果原磁盘或目录消失,缓存会话仍可能再次创建原路径,形成空库。现在每次请求先核对实际 binding:目标消失时返回 unavailable,不创建空目录;同一原目标恢复后,既有 open/status 可继续访问原件。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:无

范围

  • 关联 Issue / 需求:Mivo 图像入库与 Agent 像素读取链路的后台恢复。
  • 本 PR 包含:缓存 Library 会话重新核对 binding;原盘恢复与同路径对象替换回归;同步 Library 规则文档。
  • 明确不包含:原件暂存新协议、Mivo UI 改动、部署、合并。
  • 用户可见变化:磁盘暂时不可用时保持原库不可用语义,恢复原目标后可继续使用。
  • 是否存在 breaking change:无;原有协议形状与身份生成规则保持不变。

UI 变化

不涉及。无新增按钮、提示、设置或系统授权触发路径。

  • 引用的设计规范:不涉及。

怎么验证的

自动验证

当前 head:d7c68cd966047dfc007d1252648c075e2f8829fc(base ed4bef88b48d24bc3c397b8f94f5faed4c14368a)。旧 SHA dd7f1506 的 80 测 / related 绿 不能 复用到本 head。

隔离树 Darwin @ d7c:
pnpm --filter @cindy/maker-core exec vitest run src/agents/pi/__tests__/pi-startsession-cleanup.test.ts
104 passed
pnpm --filter desktop exec vitest run src/main/cindy-brain/__tests__/libraryBinding.test.ts src/main/cindy-brain/__tests__/libraryMigrate.test.ts src/main/cindy-brain/__tests__/librarySlot.test.ts src/main/cindy-brain/__tests__/libraryVault.test.ts
112 passed

云端 client-ci https://github.com/makecindy/cindy/actions/runs/35519789278 @ d7c
completed success(Linux 1/2+2/2、Windows 1/2+2/2 含 first-create 片、verify、verify-checks、Desktop Git)
pr-design-basis + DCO success

独立本地单审 APPROVE:/Users/praise/AI-Agent/Codex/reviews/2026-09-21-ai-ui-4728-local-review.md(无 P0/P1)。不是 GitHub Approve,不能消 REVIEW_REQUIRED。

回归使用独立临时目录:打开自定义库并写原件,移走目标后检查 open/status/write 均不重建空库;把同一目录移回后验证原件可读、重试写成功;另建同名目录则保持 binding-moved。Windows first-create 由云端 windows-latest 单测覆盖,不是 Electron 冷启。

手工验证

未执行桌面交互验证;所有文件测试使用合成临时目录,无用户素材或设置修改。

未执行的验证

Windows 同路径目录替换的 inode 识别沿用现有平台限制,测试不将 macOS 结果视为 Windows 实测。非作者维护者正式 Approve 仍缺。custom tmp 安全回收设计未做(见风险)。本批技术状态是「已审 / CI / E2E 通过,尚有明确 P2 取舍及正式批准外部依赖」,不能写无问题。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:无

影响与回滚

  • 影响范围:Desktop Library 槽的会话复用;每次请求会重新核对 binding。不可用不等于空库;不清理原件,不变更 owner/epoch 生成规则。存量插件影响:无,无需重新批准或配置。改动涉及插件能力槽,合并仍需仓库指定把关人明确放行。feat(library): 暂存未入库原件并在确认归档后释放 #4730 的批准不得复用到本 PR。
  • 回滚 / 降级方式:通过后续 PR 回退本候选(含 merge d7c68cd96 及其 Library 祖先,不是「两个提交」)。不涉及数据迁移。回退会恢复缓存 custom 根消失时可能创建空库的旧行为。
  • custom tmp:.cindy-library/tmp 的 .tmp/.stream 可能是 fsync 后、rename 前的唯一原件。现接口(含 Linux listProvableStagingOnLinux 只列表、禁止 unlink)不能证明可安全回收。本 PR 保留 custom fail-closed(跳过 path sweepStaleTmp),不按龄 unlink、不为关 P2 引入新跨平台删除。安全处置需后续明确设计。线程 PRRT_kwDOTgdRUs6kI9yC 未 resolve;是否接受残留或要求修复,须维护者具名,本包不冒充已接受。

提交前检查

  • 已 review 完整 diff(本地单审 APPROVE @ d7c;不代表服务器审查或 GitHub Approve)
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO)
  • UI 改动已在「UI 变化」注明引用的设计规范章节(不涉及 UI 则跳过)
  • 未提交凭证、令牌或授权文件
  • 已核对受影响的文档,行为变化涉及的旧结论已同步修订(不涉及则无需修改文档)
  • 已确认测试结果或说明未执行原因(104/112 绿;client-ci 35519789278 绿;独立审/E2E 已过;P2 tmp 取舍待维护者具名接受或要求修;GitHub 正式 Approve 未齐)。不是无问题。

Re-resolve the live binding on each request so a missing custom root stays unavailable instead of recreating an empty library. Same-disk restore continues through existing open/status; a rebuilt path remains binding-moved.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Unavailable cached custom sessions are not required to keep the live identity. Restore still uses the original disk object; a replaced directory stays binding-moved.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
A stale custom resolveLibraryRoot could still reach vault.open, which
recursively created the missing user parent and reported ready empty.
Custom open now refuses to recreate a vanished parent (disk-missing);
default first-time create still mkdir. keep files remain in the renamed
directory and are not treated as deleted.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
…rift

Stale resolve plus a same-path new inode is binding-moved at the mkdir
boundary. disk-missing after a granted extraDir actually revokes the slot.
Auto-open failures latch drift so status-only restore works. Keep files
stay in the renamed-away directory.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Custom skeleton mkdir is non-recursive so a vanished user parent cannot
be rebuilt as an empty library. Default roots still create recursively.
Keep files stay in the renamed-away directory.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
After the last successful inspect, hold the parent directory and compare
path identity after mkdir(root). A same-path new inode fails closed as
binding-moved; an empty root created on the replacement is rolled back
and keep files stay parked. Node has no mkdirat; this is not atomic.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Darwin mkdirat/openat (SYS_mkdirat=475) and Linux /proc/self/fd keep
ghostId/meta on the granted parent object. Missing helper fail-closed
before mutation; no path mkdir fallback. Windows custom init unsupported.
Default roots still mkdir recursively. Keep files stay parked.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
After the held parent fd is closed, custom open does not write, rename,
or unlink under this.root. Sweep unlink and persistUsage are skipped;
read-only usage scan remains. Default roots unchanged. Later library
writes still persist. Isolated D: no replacement usage.json.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
@PraiseZhu
PraiseZhu marked this pull request as ready for review September 20, 2026 11:59
@PraiseZhu
PraiseZhu requested a review from a team as a code owner September 20, 2026 11:59
@greptile-apps

greptile-apps Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

当前不宜合并:Windows custom Library 会完全不可用,且断盘恢复期间的同 ghost 并发请求可能互相作废新 session。

Findings

  1. P1 Windows 自定义库不可用 ▶
  2. P1 并发请求作废新会话 ▶
  3. P2 临时文件不再清理 ▶
  4. P2 每次打开全量扫描 ▶
Fix with agent prompt
### Issue 1
apps/desktop/src/main/cindy-brain/libraryDirFd.ts:218
Windows 是受支持的平台,但这里固定返回 `UNSUPPORTED`。`LibraryVault.open()` 对新建和已有的 custom Library 都会调用该 helper,并把失败转成 `unavailable(permission)`,因此 Windows 上所有自定义 Library 都将无法打开,而不只是存在文档所述的 inode 识别限制。需要为 Windows 提供安全实现,或让已有库走无需创建目录的验证路径。

### Issue 2
apps/desktop/src/main/cindy-brain/librarySlot.ts:317-326
同一 ghost 的请求没有串行化。两个请求可以同时持有旧 session 并完成 binding 复核;随后执行的 `teardownSession(ghostId)` 会按 Map 当前值作废另一个请求刚创建的 session。断盘或恢复期间,并发的 `open`/`status` 因此可能操作已作废的 vault、错误返回 `LIBRARY_UNAVAILABLE`,并交错撤销或重挂 extraDir grant。请把复核、teardown、创建和自动 open 放入 per-ghost 临界区,或仅条件性地 teardown 调用方原先持有的 session。

### Issue 3
apps/desktop/src/main/cindy-brain/libraryVault.ts:499
custom Library 现在始终跳过 `sweepStaleTmp()`,但普通写入和流式写入仍会在 `.cindy-library/tmp` 中创建 `.tmp` 和 `.stream` staging 文件。进程崩溃后,这些文件失去了原有的超龄清理入口,而且不计入 Library 用量,长期使用会持续占用用户磁盘。建议在完成 held-parent 和 binding 复核后,以安全的 fd-relative 方式恢复 custom tmp 清理。

### Issue 4
apps/desktop/src/main/cindy-brain/libraryVault.ts:536-553
正常 custom Library 加载后 `customOpen && this.meta` 恒为真,因此这里不再读取有效的 `usage.json`,随后每次 open 都会全量遍历素材树,并且不保存扫描结果。大库在每次打开或恢复时都会重复进行本可由账本避免的 I/O,甚至可能触发扫描保险丝。应只在首次安全初始化阶段禁止路径写入,并在 binding 复核后继续使用已有账本。

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

本 PR 为 custom Library 增加每请求 binding 复核,并通过持有父目录 fd 的方式避免断盘或同路径对象替换时创建空库;同时补充了断盘、恢复、inode 替换与授权撤销回归测试及存储规则文档。

  • cached session 会重新解析并核验 custom binding。
  • Darwin/Linux 新增 fd-relative 首次建库 helper。
  • vault 在根消失或替换时返回 disk-missing / binding-moved。
  • 当前实现会使 Windows custom Library 全部不可用。
  • session 替换仍存在同 ghost 并发竞态。
  • custom open 跳过临时文件清理,并在每次打开时全量扫描用量。
Diagram
sequenceDiagram
  participant P as 插件请求
  participant S as GhostLibrarySlot
  participant B as BindingStore
  participant V as LibraryVault
  participant H as fd-relative helper

  P->>S: open / status / read / write
  S->>B: resolveLibraryRoot
  B-->>S: custom root + realpath/dev/ino
  S->>S: confirmLiveCustomRoot
  alt 根缺失或对象已替换
    S-->>P: unavailable(disk-missing / binding-moved)
  else binding 仍有效
    S->>V: open
    V->>H: 持有 parent fd 初始化/确认目录树
    H-->>V: created / exists / unsupported
    V-->>S: ready 或 unavailable
    S-->>P: Library 结果与握手字段
  end
Loading

Reviews (1) · Last reviewed commit: "fix(desktop): skip custom first-open pat..."

Comment thread apps/desktop/src/main/cindy-brain/libraryDirFd.ts
Comment thread apps/desktop/src/main/cindy-brain/librarySlot.ts
Comment thread apps/desktop/src/main/cindy-brain/libraryVault.ts
Comment thread apps/desktop/src/main/cindy-brain/libraryVault.ts Outdated
Open an already-initialized custom library from the held parent fd
without mkdir. Missing structure on platforms without create stays
fail-closed. Teardown only the captured session. Reuse a valid usage
ledger; scan only when it is missing or corrupt. Custom tmp is not
path-unlinked.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Existing libraries open from the inherited parent handle with NtCreateFile
FILE_OPEN+ReadFile. FileIndex 0 and missing helpers fail closed. Create
stays unsupported on Windows. Linux may unlink only uuid.tmp/.stream
staging leftovers via /proc/self/fd. No path mkdir fallback.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Pretty-printed and compact meta/usage payloads both parse. Malformed
JSON or failed field checks stay CORRUPT. Do not split on the first
newline of the helper stdout.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Age and an empty streams map cannot prove a fsynced .tmp/.stream is
garbage after a crash before rename. Open no longer unlinks them.
P2_tmp stays unresolved. Unknown names are kept.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
@MagicLizi

Copy link
Copy Markdown
Contributor

这条 PR 改到插件 Library 基座(自定义库 binding 复核、fd-relative 建库、session 不可用语义)以及 Library 规则文档,需要维护者确认后再继续审查和合并。插件基座改动不因标题是 bugfix 而豁免。

请维护者直接在本 PR 上 Approve;若需要作者修改,请 Request Changes。

讨论 issue:#4772

@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:plugin-base 改动碰到插件基座(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示) labels Sep 20, 2026
Create dirs with FILE_OPEN_IF and meta.json with FILE_CREATE from the
inherited parent handle. No path mkdir, no rollback rmdir, no new
native dependency. Existing FILE_OPEN stays read-only. Missing helper
still fails closed before mutation.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Record list/resolver/control monotonic marks and dump them on waitFor
failure so Linux CI can discriminate missed 500ms poll vs unpublished
system-deny. Keep the original assertion and timeout. Mac is syntax
sanity only.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
@MagicLizi MagicLizi added the touches:large-diff 改动量较大(review-pr 自动维护,仅展示) label Sep 20, 2026
FILE_CREATE collision is exists only after FILE_OPEN of a regular file.
Reparse and unknown collisions fail closed. CreateRelative always
releases handles it still owns, including the catch path.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Keep 94f36c0 as an ancestor. Resolve the only content conflict in
pi-startsession-cleanup onto main mockRunDiscovery (list+scan) while
keeping the 132dc waitFor diagnostics and original system-deny
assertion. Library files are the automatic merge result only.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
Consume ed4bef8. Keep 94f as ancestor. Re-resolve pi-startsession-cleanup onto mockRunDiscovery (list+scan) plus 132dc diagnostics, taking main 3s durable-approval waitFor timeout. No extra Library hand edits.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
…iginals

Custom libraries skip age-based path sweep. Unlink uuid.tmp/.stream only
when nlink>=2 and ino!=0 (successful commit residue). nlink=1 after fsync
and before rename is kept. Linux custom uses held-parent fd unlinkat-style
paths; Darwin/Windows custom stay fail-closed with no path fallback.

Signed-off-by: PraiseZhu <zhuzan@xd.com>
…nique originals"

Lead ruled nlink>=2 unlink is not a sound unique-original proof
(name can be swapped after lstat; held parent does not lock names).
Restore d7c custom skip + default age sweep. Do not close P2_tmp.

Signed-off-by: PraiseZhu <zhuzan@xd.com>

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app

@MagicLizi MagicLizi removed the awaiting-discussion 等待维护者讨论(review-pr) label Sep 21, 2026
@MagicLizi
MagicLizi merged commit 6ee28d5 into makecindy:main Sep 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:large-diff 改动量较大(review-pr 自动维护,仅展示) touches:plugin-base 改动碰到插件基座(review-pr 自动维护,仅展示) touches:rules 改动碰到规则 / 规范文档(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants