Open
agent_coder
wants to merge 2 commits from
feat/251-intentional-clear into develop
pull from: feat/251-intentional-clear
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/244-part-b
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/184-autonomous-agent-runs
vvzvlad:feat/221-image-captions
vvzvlad:feat/git-sync
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/244-dataloss-bugs
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:develop
vvzvlad:feature/offline-sync
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cce539e8e2 |
fix(collab): hoist intentional-clear consume out of the store retry loop (#251)
The store-side empty-guard consumed the per-document intentional-clear flag INSIDE the bounded retry loop. consumeIntentionalClear always deletes the in-memory Map entry, but a tx rollback cannot un-delete it: attempt 1 consumed the flag then updatePage threw a transient error and rolled back; attempt 2 re-read the page non-empty, saw the flag gone, and the empty-guard silently BLOCKED the write — dropping the user's deliberate clear and defeating the retry guarantee for clears. Hoist the decision out of the loop (like consumeContributors / consumeAgentTouched): consume once into `allowIntentionalClear` before the `for`, and only read that boolean on the empty-over-non-empty branch. The single hoisted consume still drops a pending flag for a non-empty store (the "cleared then retyped" case), since every store consumes regardless of incoming emptiness. Add a regression test: arm via the real onStateless transport, updatePage throws once then succeeds, assert it is called twice and the retry writes the empty doc (the clear survives). It fails on the old consume-in-loop ordering (updatePage called once) and passes after the hoist. Document the known fail-safe limitation near the TTL constant: if document ownership transfers / a node crashes between the stateless signal and the debounced store, the in-memory flag is lost and the clear is silently not applied (the doc reloads non-empty) — fail-safe, content is never destroyed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3fdb1e05a4 |
feat(collab): persist a deliberate page clear via an intentional-clear signal (#251)
The #248 store-side empty-guard (onStoreDocument) unconditionally refuses to overwrite non-empty persisted content with an empty document, because a momentarily-empty live Y.Doc is indistinguishable from a real clear at the store layer. That correctly blocks glitches/bad-merges, but also blocks a user who genuinely wants to empty a page. This re-introduces a WORKING, narrow, non-spoofable exception (the dead context.intentionalClear hatch #248 removed never had a real channel). Definition of an intentional clear (client, IntentionalClear editor extension): a LOCAL user transaction (docChanged, NOT a remote y-sync change — filtered via isChangeOrigin) that reduces a non-empty doc to the empty single-paragraph shape. This is exactly the select-all + Delete/Backspace keystroke path. Transport (option b — hocuspocus stateless message): on that transition the client sends a `{type:'intentional-clear'}` stateless message. The server (PersistenceExtension.onStateless) records a short-lived (TTL 60s > 45s maxDebounce), single-use "pending clear" flag keyed by the connection's document. The next debounced onStoreDocument consumes it on the empty-guard branch to let that one empty write through. Why this is the right channel and non-spoofable: - Yjs transaction origin/metadata does not survive to the server store; awareness is per-connection and racy. A stateless message ties the signal to a specific clear, survives the debounce, and rides the authenticated connection. - The document is taken from the connection, never the payload, so a client cannot target another page. - The flag is read ONLY on the empty-over-non-empty branch, so the worst a forged signal can do is clear a page the connection may already edit; it can never force or alter a non-empty write. Read-only connections cannot arm it. Every non-empty store drops a pending flag, so "cleared then retyped" leaves nothing usable; the flag is single-use and TTL-bounded. NOTE: #248 is not yet on develop, so the empty-guard block is included here as the foundation this exception extends. If #248 lands first this rebases cleanly (the guard logic is identical; the #251-unique additions are the exception, onStateless, the pending-flag state, and the client extension). Tests: - Server (real transport path, not a hand-poke): onStateless sets the flag with the exact client payload, then the debounced onStoreDocument persists the empty doc; plus single-use consumption, read-only rejection, non-empty-store drops the flag, and the unchanged #248 guard tests (empty-over-non-empty blocked, empty-over-empty allowed). - Client: a real Editor + the actual selectAll+deleteSelection command emits the signal; typing / non-emptying edits / already-empty docs do not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |