Skip to content

fix(folder): compile recursive blob cleanup - #190

Closed
PeterGuy326 wants to merge 5 commits into
mainfrom
codex/fix-177-recursive-delete
Closed

PeterGuy326 wants to merge 5 commits into
mainfrom
codex/fix-177-recursive-delete

Conversation

@PeterGuy326

@PeterGuy326 PeterGuy326 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Requirement and behavior

Refs #177. This canonical PR carries the recursive object-cleanup implementation from #186 and corrects its three invalid assignments from pgx.Rows.Close(), which returns no value.

Recursive folder deletion obtains owner-scoped storage keys from DELETE ... RETURNING inside the database transaction, removes objects only after commit, and logs the key when object deletion fails. Explicit memory-forget and task-state guards remain in place. The documented best-effort crash window remains accepted; this is not guaranteed physical erasure or a crash-recovery queue.

Reviewed follow-up: a9f7238e8fcfbb031c84912a2923a6e6af0a6578, parent e76d4b03a4d3681db868bcd2587e54a56cbceda1. It consolidates the duplicate Unreleased Fixed subsection, adds a bounded duplicate-subsection regression to the existing release guard, and restores the memory-lifecycle, folder_id SET NULL and subfolder ON DELETE CASCADE rationale. Production code differs from the parent only in comments; queries, API, dependencies and the lifecycle CI implementation are unchanged.

Validation ledger

Check Environment / observation Result
Duplicate changelog regression Original #190 changelog produces ERROR: duplicate changelog subsection at line 52: ### Fixed; corrected candidate preserves every bullet and one Fixed subsection Red/green verified
Complete release guards Pinned Debian Linux container, readonly checkout, executable temporary filesystem; exact command below PASS; separately reproduced by an independent automated verifier without source changes
Source review / hygiene Independent review of a9f7238; folder production content identical after removing comment lines; bash syntax, ShellCheck, gofmt and git diff --check HEAD^ HEAD PASS
Historical live blob cleanup Exact parent e76d4b0, required lifecycle job 102747493733, named TestRecursiveDeleteCleansBlobs RUN/PASS (0.04s), no skip PASS on that parent, not automatically a new-head result
New-head CI Exact a9f7238: CI, Agent-memory validation, security SUCCESS; new-head lifecycle job 102807313835 also emits named TestRecursiveDeleteCleansBlobs PASS (0.04s), no skip
Local PostgreSQL/MinIO rerun Not repeated for this comment/changelog/test-guard follow-up NOT RUN locally

The new changelog check permits the same subsection in different releases and rejects repeated subsections within one release, including duplicates separated by another category. Sequential changelog integration with the other feature PRs remains a coordinator-owned step; no other feature's entry is copied into this PR.

Reproduce the complete release guards

From this candidate's repository root, with a Linux Docker engine able to bind-mount the checkout:

docker run --rm --network none --read-only \
  --tmpfs /tmp:rw,exec,size=128m \
  --volume "$PWD:/src:ro" \
  debian@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171 \
  bash /src/scripts/test_release_guards.sh

Expected and observed: exit 0 and PASS: release source, notes, asset-set and checksum guards fail closed.

No extra environment variables, real Git refs or Git-directory mount are required. The script creates its own temporary fake git and supplies its PATH/FAKE_* environment for Git-object guard cases. The tmpfs must allow execution: a default noexec tmpfs prevented that fixture from executing in the first independent attempt and surfaced as a misleading missing-tag error. With exec enabled the unchanged script passed. The earlier native macOS attempt also stopped at BSD find's unsupported -printf; the pinned Linux image provides the expected GNU utilities. Containers and their temporary files are removed on exit. No newly built macOS Go executable was run.

Real object-store evidence boundary

HTTP, CLI and MCP lifecycle job 102807313835 belongs to successful run 34457532100 on exact published head a9f7238e8fcfbb031c84912a2923a6e6af0a6578. It starts real isolated PostgreSQL and MinIO, then emits named JSON RUN and PASS (0.04s), with no skip, for TestRecursiveDeleteCleansBlobs, followed by lifecycle PASS. The earlier job 102747493733 remains historical evidence for parent e76d4b03a4d3681db868bcd2587e54a56cbceda1 only.

This proof comes from the required HTTP, CLI and MCP lifecycle job in memory-validation.yml, not the PostgreSQL integration job or the ordinary ci.yml Go unit job. The latter may skip the S3 test when its endpoint is absent. scripts/acceptance_agent_memory.sh lines 894–907 invoke the exact test with go test -json and configured PostgreSQL/MinIO values; the following jq predicate requires Action=pass for that exact name, rejecting skips, while pipefail preserves producer errors.

On a Linux host with Go, Docker Compose, curl and jq, ./scripts/acceptance_agent_memory.sh reproduces the real-service scenario and owns its disposable _test database, MinIO stack and cleanup. The historical local skipped invocation remains NOT VERIFIED and is not counted as this CI result. The earlier independent 16-case synthetic gate verification is supplemental shell/JSON evidence, not another real-storage run.

Integration and attribution

The PR remains Draft against main. #192 has merged as 87db0dfe0507be2190fe2fdcce0e267be8224f4d; this exact follow-up does not merge that main commit into feature history. The coordinator retains final integration, ancestry and merge decisions. Automated source review and test replay are not a formal human APPROVE.

Original implementation: #186 by @sun-970 (Li Yuanyang; commits attributed to liyuanyang). This PR retains that contribution with maintainer compile corrections and bounded follow-up cleanup. Contributor credit must be retained in any eventual squash commit; no history or approval is rewritten here.

Risk and rollback

The follow-up does not change recursive deletion behavior. Reverting it restores the prior changelog layout, comments and guard coverage. The underlying object cleanup remains best-effort after commit: failure does not undo the database deletion, and a process crash can leave objects requiring operator follow-up, as documented in docs/DEPLOYMENT.md. No production erasure or issue-closure claim is made.

@PeterGuy326

Copy link
Copy Markdown
Collaborator Author

CI baseline dependency: current main Web audit is independently red on published dependency advisories. Separate #192 refreshes only the Web lockfile. This PR is now stacked on #192; merge/rebase #192 first, then retarget #190 to main. The #190 application diff remains folder-cleanup-only.

@PeterGuy326

Copy link
Copy Markdown
Collaborator Author

Independent automated preflight — PASS, bounded #177 cleanup scope

Candidate: e76d4b03a4d3681db868bcd2587e54a56cbceda1. This is automated preflight, not a human APPROVE or a merge decision.

Independently inspected the production wiring and transaction boundary: storage keys come from the owner-scoped DELETE ... RETURNING; blob deletion happens only after the database transaction succeeds. Existing memory/task-state guards remain. The documented best-effort crash/failure window is not an atomic or guaranteed-erasure promise.

Independently inspected the new live gate and test: set -euo pipefail preserves the test exit code through tee; the JSON predicate requires the specific test's PASS event. Setup creates the required workspace, reads and closes real object contents before deletion, and checks typed NoSuchKey afterward rather than accepting any storage error.

Rechecked git diff --check, bash -n and shellcheck: pass. Read the exact-head required lifecycle CI log: the named TestRecursiveDeleteCleansBlobs ran and passed, with no skip, followed by lifecycle PASS. All 20 current checks are SUCCESS. Worker-local real-service/negative-control results are supplemental evidence, not represented as coordinator-executed tests.

The shared Web lockfile is identical to #192. This PR is still based on #192: land that baseline through its independent review gate first, then retarget to main and verify the final candidate. No approval bypass, production deletion, or release is claimed.

@Bindy-lbb

Copy link
Copy Markdown
Collaborator

Review summary (verified independently)

  • Head SHA confirmed at e76d4b03 (matches). CI confirmed 20/20 green independently.
  • Reviewed folder.go recursive delete + folder_delete_integration_test.go: transaction-scoped DB delete with RETURNING storage_key, best-effort blob cleanup after commit (logs rather than fails on error) — matches the documented tradeoff. The new test genuinely uses a real pgxpool connection and a real MinIO client (not mocks), and asserts both that DB rows are gone and that a real NoSuchKey comes back from MinIO after delete.

Gap found: this test requires MEM_TEST_DB and MEM_TEST_S3_ENDPOINT. ci.yml sets the former but never configures a MinIO service or MEM_TEST_S3_ENDPOINT, so this acceptance test silently skips in CI on every run (consistent with the PR's own validation ledger marking this item as not verified). Please add a MinIO service to CI so this test actually executes before treating the 20/20 green as covering it.

Same base/retarget blocker as #189 — this PR bases off #192's branch, not main.

@PeterGuy326
PeterGuy326 marked this pull request as draft September 10, 2026 05:53
@PeterGuy326

Copy link
Copy Markdown
Collaborator Author

Review follow-up — correcting the MinIO evidence

@Bindy-lbb The base/ancestry concern is confirmed; this PR is now Draft until #192 lands in main and the retarget/integration/fresh-review sequence is completed. There is one CI distinction to correct: the ordinary Go unit job in ci.yml can skip the storage test, but the separate required HTTP, CLI and MCP lifecycle job in memory-validation.yml runs it with real PostgreSQL and MinIO.

I directly checked job 102747493733, run 34438164247, attempt 1, for exact current head e76d4b03a4d3681db868bcd2587e54a56cbceda1. It logs isolated PostgreSQL/MinIO startup, then on 2026-09-10 at 04:49:47 UTC:

Action=run, Test=TestRecursiveDeleteCleansBlobs
--- PASS: TestRecursiveDeleteCleansBlobs (0.04s)
Action=pass, Test=TestRecursiveDeleteCleansBlobs, Elapsed=0.04
PASS: isolated HTTP, CLI and MCP Agent-memory lifecycle acceptance

The structured log events above are condensed to their relevant fields. The old PR-body V5 was a historical local skip, not a description of this current-head lifecycle run; the body now separates these facts and links the job. Thus “always skipped in CI” does not apply to this head.

A separate automated checker also extracted the actual shell/JSON gate from script blob afaba72fab518d7091fc2bdffb4fd7cfb744d9b3, including strict mode and cleanup trap: 16/16 synthetic cases met expectations. Correct named PASS succeeds; skip, failure, missing/wrong/malformed results and nonzero producer exit after PASS all fail closed. This synthetic check is not another live-storage E2E run or a human review. Local Docker is unavailable, so no local live-service rerun is claimed. No code change was needed for this gate.

Author-posted preflight comments are supplemental automated evidence only, never a substitute for a non-author APPROVE. Contributor credit for @sun-970 / Li Yuanyang remains in the body.

Base automatically changed from codex/fix-web-audit to main September 10, 2026 07:26
@sun-970

sun-970 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Thanks for the detailed PR description and thorough validation ledger. Here's my review:

Overall: The approach is sound — collecting storage_key via RETURNING inside the transaction and deleting blobs after commit is the right call. The blobDeleter interface is clean and the nil-store pattern for tests is pragmatic.

Issues found:

  1. Context cancellation after commit (folder.go ~L684-693): After the transaction commits, if the context is cancelled (e.g., HTTP timeout), the remaining blob deletions silently fail without even a warning log. Consider using context.WithoutCancel(tx.Context()) or a detached context.Background() for the post-commit cleanup loop, since we've already committed to the deletion and want best-effort completion with logging.

  2. Sequential blob deletion: For a folder with hundreds of files, blobs are deleted one-by-one sequentially. This is acceptable for best-effort cleanup but could be slow. A sync.WaitGroup with bounded concurrency (e.g., 8 workers) would be a reasonable improvement. At minimum, consider logging the total count and elapsed time at INFO level so operators can spot slow cleanups.

  3. package-lock.json mixed in: This PR includes vitest 4.1.10→4.1.11, js-yaml 4.3.1→4.3.2, and postcss-selector-parser version bumps that are unrelated to recursive folder deletion. These should be in a separate commit/PR to keep the diff focused and reviewable.

  4. Missing partial-failure test: The integration test covers the happy path well (including the _test database guard — nice). But there's no test for the case where some blob deletions succeed and others fail. A test with a mock blobDeleter that fails on specific keys would verify that: (a) the DB deletion still succeeds, (b) failures are logged, and (c) remaining keys are still attempted.

  5. PR description density: The validation ledger is impressively thorough, but the description is very long. Consider moving the detailed reproduction steps and historical evidence into a linked gist or the project wiki, keeping the PR body focused on what changed and why.

Minor nits:

  • The rows.Close() is called explicitly before rows.Err() — this is correct but could use a defer rows.Close() right after the query for safety against future refactors.
  • The orphanKeys variable name is slightly misleading — these aren't orphans yet, they're keys to delete. Consider keysToDelete or deletedKeys.

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST-CHANGES — the guard looks at the wrong column, and this PR makes the miss unrecoverable

First, what is clearly good and I want on the record: replacing the shared p + ".tmp" staging
path with os.CreateTemp fixes a real collision bug; rows.Err() and rows.Close() are both
handled; a nil store is a clean way to keep unit tests out of object storage; and the doc
comment honestly states the crash window instead of claiming atomicity.

S2-1 — containsMemoriesTx checks where memories live, not what they cite

server/internal/folder/folder.go:691 — the only thing standing between
DELETE ?recursive=true and physical destruction is:

AND (m.path = $2 OR left(m.path, length($2) + 1) = $2 || '/')

That is a prefix match on memories.path. It never consults memories.source_file_id, which
is declared in 0008_agent_memories.sql:45 as uuid REFERENCES files(id) ON DELETE SET NULL.
Those two things are not the same relation: a memory at /Work/task can cite a file at
/Photos/2012, and the guard will not see it.

So for that file: the row is deleted (FK silently NULLs source_file_id), the blob is destroyed,
the memory row survives as active, and source_file_sha256text NOT NULL DEFAULT '' with
only a format CHECK, no FK, nothing that re-validates it — keeps naming an object that no
longer exists.

I checked whether this predates your PR, and partly it does. On main,
server/internal/file/file.go:675 already destroys the blob on single-file delete, with no
provenance guard at all and the error swallowed silently at :676-680 (_ = derr, not even a
log). I am not asking you to fix file.Delete here. What this PR changes is the blast radius
and the recoverability: ?recursive=true takes a whole subtree in one call, and where main's
DB-rows-only behaviour left the bytes in the bucket so an operator could still prove or rebuild
the link, after this PR there is no recovery path. Precedent is not a defence when the
operation is the bulk form of it.

Any one of these resolves it:

  • make the guard referential, or
  • restrict cleanup to unreferenced files, e.g. in the DELETE … RETURNING storage_key add
    AND NOT EXISTS (SELECT 1 FROM memories m WHERE m.source_file_id = f.id AND m.lifecycle_status IN ('active','archived')), or
  • record explicitly in the changelog that provenance loss on cross-path citations is accepted.

S2-2 — the DEPLOYMENT.md justification is not supported by the schema

New text: orphans are "unreferenced — no live database row points at them — and are safe to
leave in place." After the FK NULLs the pointer, that is literally true of the pointer columns
and false of the situation: the live memory row still carries source_file_sha256, which is
exactly what cmd/mem/cmds_memory.go:476-480 prints to users as provenance. Please reword to
say what survives and what is destroyed — right now the doc launders an unstated data-loss
tradeoff into a reassurance.

S2-3 — post-commit cleanup runs on the request context

if txErr != nil { return txErr }
if s.store != nil {
    for _, key := range orphanKeys {
        if derr := s.store.Delete(ctx, key); derr != nil {
            s.log.Warn("folder.blob_delete_failed", …)

ctx here is the handler's context, and the DB transaction has already committed. A client
disconnect or an HTTP server timeout mid-delete fails every remaining key with
context canceled, each producing one Warn, with no counter, no summary, and nothing that
would retry — so the failure mode is a silent, unbounded pile of orphaned blobs precisely in
the case where there are the most of them. Subtree deletes are the slow ones, which is the
worst pairing. You already log a Warn (better than file.go, which logs nothing); what's
missing is a detached context and an aggregate. There is an in-repo precedent for the
detachment: indexgeneration/store.go uses defer tx.Rollback(context.WithoutCancel(ctx)).

S2-4 — no test observes the guard/cleanup interaction

TestRecursiveDeleteCleansBlobs creates zero memory rows, so it exercises the happy path
only. The one test that does create memories, TestMemoryPathLifecycleIntegration
(folder_test.go:228), is switched to New(pool, nil, nil)store == nil, so cleanup is
skipped there by construction. Net effect: no test in the PR would fail if containsMemoriesTx
were deleted outright.

Please add the case the design actually needs: an active memory whose path is outside the
deleted subtree but whose source_file_id points at a file inside it — assert whatever you
decide S2-1 resolves to, so the invariant is pinned either way. Also worth checking as negative
controls: with AND user_id = $1 dropped from the DELETE, and with the
left(path, length($3) + 1) = $3 || '/' arm dropped, does anything fail? I did not run them;
if not, the query's two most safety-relevant predicates are unasserted.

Not blocking, for context

Your acceptance wiring does land: TestRecursiveDeleteCleansBlobs runs under
memory-validation.yml:191scripts/acceptance_agent_memory.sh, which exports
MEM_TEST_S3_ENDPOINT itself, and it is green at a9f7238e8 (6 runs, 0 failures). Heads-up
that any rebase onto current main will turn that job red for a reason that is not yours:
pull access denied for minio/minio, same as #198/#199/#203/#205. Also
mergeable_state=behind.

Provenance

Static read of head a9f7238e8 diff plus folder.go, file.go, 0008_agent_memories.sql and
cmds_memory.go on main 3c13f04. I ran no Go tests and no MinIO/PostgreSQL instance, so
the negative-control questions above are questions, not findings. If you disagree with my read
of S2-1 I would want the reachable-scenario check to come from a test rather than from me.

@waterbro-8

Copy link
Copy Markdown
Collaborator

Review: the guard checks the wrong column, and this PR makes the miss unrecoverable

First, what is right here, because it is most of the PR. DELETE ... RETURNING storage_key, best-effort removal after commit, the narrow blobDeleter interface, nil store skipping cleanup, and the honest note in docs/DEPLOYMENT.md about the crash window — all of that is the correct shape. TestRecursiveDeleteCleansBlobs asserting a real NoSuchKey against live MinIO is exactly the kind of test this needs. I am not asking you to redesign any of it.

The problem is one query that this PR did not touch, and the way this PR changes its blast radius.

The guard asks where a memory lives, not what it cites

server/internal/folder/folder.go:754 on this head:

SELECT EXISTS (
  SELECT 1
    FROM memories AS m
    JOIN workspaces AS w ON w.id = m.workspace_id
   WHERE w.resource_owner_user_id = $1
     AND m.lifecycle_status IN ('active', 'archived')
     AND (m.path = $2 OR left(m.path, length($2) + 1) = $2 || '/')
)

memories.path is the folder the memory is stored at. It is not the file the memory cites. Those are different columns, and server/internal/db/migrations/0008_agent_memories.sql:45 defines the second one:

source_file_id  uuid REFERENCES files(id) ON DELETE SET NULL,

So the sequence is:

  1. A memory is stored at /Notes (m.path = '/Notes') but its source_file_id points at a file inside /Docs/Child.
  2. Recursive delete of /Docs calls containsMemoriesTx(ctx, tx, userID, '/Docs', true). That query matches on m.path LIKE '/Docs%'no match → returns false.
  3. ErrContainsMemories is not returned. The delete proceeds.
  4. DELETE FROM files WHERE ... left(path, length($3)+1) = $3 || '/' removes the file rows.
  5. ON DELETE SET NULL fires. That memory's source_file_id is silently set to NULL.

The comment right above the guard states the intent:

// A folder operation must never become an implicit memory
// deletion. The caller has to use the memory lifecycle's
// explicit forget operation first.

The guard does not enforce that intent. It enforces "no memory is stored in this subtree", which is a different and much weaker claim.

Why this PR is the reason it needs fixing now

Before this change, recursive delete removed DB rows and left the objects in the bucket. That was wrong, but it was recoverable: an operator could reconcile orphan objects against storage_key and reconstruct what had been referenced.

After this change the objects are actually deleted. orphanKeys lives only in the stack frame of one Delete call. When the process exits, the set of keys that were removed exists nowhere — not in a log (the failure path logs, the success path does not), not in the DB, not in the bucket.

So this PR converts a silent provenance loss into an irreversible silent provenance loss. That is the specific reason I am blocking on this line rather than filing it as a follow-up.

What I am asking for

Extend the recursive check to also cover citation, e.g.:

SELECT EXISTS (
  SELECT 1
    FROM memories AS m
    JOIN workspaces AS w ON w.id = m.workspace_id
    JOIN files AS f ON f.id = m.source_file_id
   WHERE w.resource_owner_user_id = $1
     AND m.lifecycle_status IN ('active', 'archived')
     AND (f.folder_id = $2 OR f.path = $3
          OR left(f.path, length($3) + 1) = $3 || '/')
)

with $2 / $3 the deleted folder's id and path. Rows with source_file_id IS NULL drop out of the inner join, which is the behaviour we want — a memory with no cited file has nothing to lose here.

And a test that fails without the fix: put a file in the deleted subtree, put a memory outside that subtree whose source_file_id points at it, recursive-delete, expect ErrContainsMemories. Right now TestRecursiveDeleteCleansBlobs cannot catch this because it never creates a memory at all.

Two smaller things

  • New(pool, store, logger) changes the signature at 12 call sites and 11 of them pass nil, nil. That ratio is a signal: almost every caller has no storage. Consider an options struct so the next dependency does not repeat the sweep.
  • web/package-lock.json is +50/−50 of unrelated vitest / js-yaml / postcss bumps. Please drop it from this PR — it is identical to the copy in fix(ingest): bound cursor lock waits so a wedged peer cannot hang ingest #191 (same blob 28a95f0b2f19) and belongs in its own dependency change.

@waterbro-8

Copy link
Copy Markdown
Collaborator

关闭:两份并行实现收敛到 #210

按清理指令处理 #177 的重复实现:这一条关闭,#210 保留。先把你这边做对的地方记在案上,再讲为什么收敛到 #210

#190 里值得保留的判断

  • RETURNING storage_key 在删除同一条语句里取回 key,省掉一次往返 —— 思路是对的,而且比 fix(folders): clean up blob storage on recursive folder delete (#177) #210 的"先查后删"少一次往返。
  • 提交后再做 blob 清理、失败只告警不回滚,与 file.Delete 既有的 best-effort 形状一致。
  • 文档注释如实写明崩溃窗口,没有把"最终一致"写成"原子"。
  • docs/TESTING.mdscripts/acceptance_agent_memory.shscripts/test_release_guards.sh 里补的验收步骤是这份 PR 独有的增量。

这些设计判断 #210 全部继承了(docs/DEPLOYMENT.mdCHANGELOG.md 对应段落),没有丢。

为什么保留 #210 而不是 #190

实测两份 head 的代码,三处硬差异:

  1. 构造函数fix(folders): clean up blob storage on recursive folder delete (#177) #210New(pool, opts ...Option),向后兼容;fix(folder): compile recursive blob cleanup #190 改成 New(pool, store, log) 破坏性签名,必须同步改调用方(你的 head 也确实改了 server/cmd/memd/main.go 和多个 *_integration_test.go)。
  2. 清理用的 ctx(决定性的那条)fix(folders): clean up blob storage on recursive folder delete (#177) #210context.WithoutCancel(ctx) + 30s 超时;fix(folder): compile recursive blob cleanup #190 把请求的 ctx 直接传进 s.store.Delete。请求在提交后取消(客户端断开、CLI 被 Ctrl-C、上游超时)时,fix(folder): compile recursive blob cleanup #190 的 blob 清理会静默失效——而失败只写 WARN,现场不会留下任何痕迹。这正是本 PR 想修的那类"删了行、留了 blob",只是换了个触发路径。
  3. 改动面fix(folders): clean up blob storage on recursive folder delete (#177) #210 动 5 个文件、单个 commit;fix(folder): compile recursive blob cleanup #190 动 12 个文件。

需要说明的是:我没能把两份都跑起来对比(本机跑不了 CI 那条 HTTP, CLI and MCP lifecycle,见下),所以第 2 条是我逐行读 folder.goDelete 收尾路径得出的,不是运行时观测。

关闭 #190 不撤掉我在这条 PR 上给的评审意见

S2-1 仍然有效,而且两条实现都同样带着它,因为它来自 main 上既有的代码:containsMemoriesTx 只做 memories.path 的前缀匹配,从不查 memories.source_file_id0008_agent_memories.sql:45 声明为 REFERENCES files(id) ON DELETE SET NULL)。

后果:记忆挂在 B 目录、引用的是 A 目录的文件时,递归删 A 会把 blob 物理删除、source_file_id 被静默置 NULL、记忆行仍是 activesource_file_sha256 继续指着一个已经不存在的对象。

这一条我已经挂在 #210 上作为合并前事项,不会随本 PR 一起消失。

顺带记录两件与代码质量无关、但当时确实挡着这台车的事

(本次关闭不构成代码评审投票,不代表合并或验收任何东西。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants