Skip to content

feat(copilot): doc-management tools + Requesty/GLM reasoning-leak fix - #2

Merged
zebster-cmd merged 21 commits into
requesty-provider-specfrom
feat/copilot-doc-management-tools
Jul 15, 2026
Merged

feat(copilot): doc-management tools + Requesty/GLM reasoning-leak fix#2
zebster-cmd merged 21 commits into
requesty-provider-specfrom
feat/copilot-doc-management-tools

Conversation

@zebster-cmd

Copy link
Copy Markdown
Owner

Batches two changes for a single self-host image build + deploy. (Stacked on top of #1, so this PR includes #1's Track B commits; merging this closes #1.)

Track A — copilot doc-management tools

Lets the AI manage everything you can attribute to a doc, plus links — all server-side, YJS-direct, delta-push (mirroring DatabaseWriter), no affine_doc_loader changes.

  • Readers/writers (core/doc/): DocPropertiesReader/Writer (tags, custom properties, title, trash, journal, primary mode, favorite — across root-meta, docProperties, docCustomPropertyInfo, and the per-user favorites doc), DocLinksReader (outgoing + backlinks via the indexer, no per-doc scan), DocLinksWriter (embed-block + inline @-ref create/remove/retarget, create_doc_and_link), shared yjs-delta helper.
  • 4 tools: doc_properties_read/doc_links_read (ungated, Doc.Read), doc_properties_update/doc_links_update (gated dev||canary, Doc.Update). Name-based referencing with id fallback + disambiguation; explicit create_tag/define_property; batched, up-front-validated writes.
  • Wired into tool-runtime.ts + PromptToolsSchema + built-in.json (needs the CI native rebuild to take effect).
  • Out of scope: edgeless/canvas.

Track B — Requesty/GLM reasoning leak (#1)

ThinkTagSplitter separates inline <think>…</think> from content (streamText/streamObject → reasoning channel; text()/extractTextResponse exclude reasoning), so code artifacts are reasoning-free. Live-confirmed on the deployment.

Verification

Subagent-driven: every task implement → review → fix → re-review; final whole-branch review (found + fixed a set_title dual-write bug). All logic verified via tsx harnesses + clean tsc --noEmit + oxlint/eslint — the ava suite runs in CI (Node 22), since local Node 24 can't bootstrap ava's prelude. OpenSpec changes under openspec/changes/.

🤖 Generated with Claude Code

zebster-cmd and others added 21 commits July 14, 2026 18:46
Initialize OpenSpec (spec-driven) and gitignore .claude/ (local Claude
Code state). Add the change 'fix-requesty-reasoning-leak' (proposal,
design, specs, tasks) for capability copilot-reasoning-separation:
isolate model reasoning ("thinking") from content and artifact text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…acts

Add ThinkTagSplitter (stateful, chunk-boundary-tolerant) + stripThinkTags.
Wire it into the native adapter: streamText/streamObject route inline
<think>…</think> onto the reasoning channel; text() excludes reasoning
(emitReasoning:false). extractTextResponse now keeps only text parts and
strips inline <think> from them, so code_artifact HTML is reasoning-free
whether the model separates reasoning natively or emits it inline
(e.g. GLM via Requesty).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splitter: basic/multiple blocks, split-at-every-offset invariance,
unterminated/partial tags, think-free passthrough. Integration:
streamText/streamObject route inline <think> to the reasoning channel
with clean content, text() excludes reasoning, extractTextResponse drops
reasoning + strips inline tags, and code_artifact HTML is reasoning-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proposal/design/specs/tasks for capabilities copilot-doc-properties and
copilot-doc-linking: let the AI manage a doc's tags, custom properties,
core metadata, journal/mode, favorite, and its links/backlinks — all via
server-side YJS-direct writes (DatabaseWriter delta-push pattern).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add YjsDeltaWriter/applyYDocDelta (generalized DatabaseWriter delta-push,
handles not-yet-existing docs), doc-properties-types (storage doc-id
builders, property-type set, view shapes), and DocPropertiesReader:
aggregates a doc's title/trash/tags (root doc), journal/mode/custom
values (docProperties, resolved via docCustomPropertyInfo), and favorite
(per-user userspace doc) into one resolved view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixtures build the four attribute docs (root meta, docProperties,
docCustomPropertyInfo, favorites). Reader specs cover tag resolution
(incl. unresolved-id fallback), journal/mode/custom values resolved to
name+type, missing-row defaults, and per-user favorite state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rty/favorite ops)

Adds DocPropertiesWriter (extends YjsDeltaWriter) implementing the
doc_properties_update op set: set_title/set_trash (root doc pages[]),
set_journal/set_mode (docProperties), add_tag/remove_tag/create_tag
(root doc tag options, name-or-id resolution with disambiguation
errors), set_property/define_property (docCustomPropertyInfo, rejects
select/multi-select), and set_favorite (per-user favorites doc,
soft-delete on unfavorite). DocPropertiesCodec encodes/decodes
custom:<id> string values by declared type.

Ops touch up to four independent docs, so applyOps resolves and
validates every op up front (including within-batch create_tag ->
add_tag / define_property -> set_property chaining) before pushing any
delta, then groups mutations per affected doc - giving all-or-nothing
batch semantics without real cross-doc transactions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ava specs for DocPropertiesWriter covering: the string codec
(text/number/checkbox/date/tags); set_title/set_trash/set_journal
(incl. "" clearing and malformed-date rejection)/set_mode;
create_tag/add_tag/remove_tag (by name and id, missing/ambiguous-name
errors); define_property/set_property (undefined-property and
unsupported-type errors); set_favorite (create + soft-delete);
same-batch create_tag->add_tag chaining; mid-batch-invalid-op abort
(no partial write); a round-trip across all four docs in one batch;
and a CRDT concurrency test (writer delta + independent simulated
client edit merge losslessly in both apply orders).

Verified via a temporary tsx harness against src/prelude.ts (ava
itself doesn't run on this machine's Node 24) - all 29 assertions
green - and `tsc --noEmit`, `oxlint --deny-warnings`, and `eslint`
clean on both new files.

Marks openspec tasks.md groups 3, 4, and 9.1/9.2 done (9.1/9.2 covered
at the writer/reader unit level; the doc_properties_update/read tool
wrappers are separate Track A tasks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ous-name paths

doc-properties-writer's add_tag/remove_tag cast page.get('tags') to a Y.Array
unconditionally, throwing a raw TypeError on legacy/externally-created docs
whose meta.pages[] entry has no tags field; ensureTagsArray() now creates and
attaches one on demand, mirroring the reader's tolerance. set_favorite always
wrote index: 'a0', colliding whenever more than one doc was favorited;
nextFavoriteIndex() now reads existing favorite indices and picks one that
sorts after them. Also removes the dead NotFoundException re-checks inside
the root-doc mutators (already guaranteed by the up-front pageMeta check),
and adds spec coverage for the missing-tags path and the remove_tag/
set_property ambiguous-name error paths (add_tag's was already covered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DocLinksReader.read resolves a doc's outgoing links (block search,
docId==X ∧ exists refDocId) and backlinks (aggregate on docId where
refDocId==X), with best-effort title resolution via the indexer's doc
table. Injects the real IndexerService but only calls search/aggregate,
so tests pass a fake indexer instead of standing up a search provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DocLinksWriter.applyOps mutates a source doc's own binary (delta-against-
state-vector push, via YjsDeltaWriter) to implement create_link (embed
affine:embed-linked-doc block, default; or inline @-reference space-delta
at an anchor block), remove_link, retarget_link, and create_doc_and_link
(spins off + registers a new doc via DocWriter.createDoc, then links to
it). Every op is validated against a read-only snapshot before any doc is
created or delta pushed, so an invalid op in a batch aborts the whole
batch with no partial write - mirroring DocPropertiesWriter.

Includes the links-doc test fixture (page -> note -> paragraphs/embeds)
and full writer specs covering every op, its doc-wide fallback search,
missing-target/anchor errors, batch-abort semantics, and CRDT-merge
concurrency safety.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groups 5 (links reader) and 6 (links writer) are implemented and
verified (tsx + typecheck); see the corresponding feat commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- create_doc_and_link (embed mode) now resolves and validates the source
  doc's note block during the up-front validation pass, before
  DocWriter.createDoc runs, so a source doc missing an affine:note block
  can no longer leave behind an orphaned, registered-but-unlinked doc.
- remove_link/retarget_link given both an explicit blockId and a target
  doc id (targetDocId / fromTargetDocId) now verify the resolved embed
  block's prop:pageId actually matches before mutating/deleting it,
  throwing a clear error naming both ids on mismatch instead of silently
  acting on the wrong link.
- Align the copilot-doc-linking spec's inline @-reference scenario with
  design.md's resolved decision: the anchor names a target block (no
  character-offset parameter) and the reference is appended at the end
  of that block.

Verified via a throwaway tsx harness running all 21 ava specs (18
existing + 3 new) against Node 24, plus a clean
`tsc --noEmit`/oxlint/eslint pass on the touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the four AI-facing defineTool wrappers over the existing doc-properties
and doc-links readers/writers: doc_properties_read, doc_properties_update,
doc_links_read, doc_links_update. Mirrors the database-read/-update tool
shape (build<X>Handler + create<X>Tool, permission check via
PermissionAccess.doc().can('Doc.Read'|'Doc.Update'), thrown errors converted
to toolError).

doc_links_update owns name->id target resolution (deferred from
DocLinksWriter per design.md Decision 4/5): a target string that exactly
matches a doc id wins, otherwise it's matched against doc titles in the
workspace root's meta.pages[] via the new resolveDocIdsByTitle() helper on
doc-properties-reader.ts; zero/ambiguous matches surface as a toolError
before any write is attempted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ava specs for the four tool wrappers: permission-denied -> toolError for
all four; reader-returns-null and writer-throws -> toolError naming the
doc id/op for doc_properties_read/update; doc_links_update's target
resolution (exact id, exact title, missing target, ambiguous title listing
candidate ids) and create_doc_and_link's root-doc-skip path. All fakes -
no real storage/indexer touched.

tsc clean; a scratch tsx harness (ava can't run on local Node 24) confirmed
all 15 assertions pass before this file was written and the harness deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad-throw tests

- Fix doc_links_update's top-level tool description to name the actual
  camelCase op fields (target/fromTarget/toTarget) instead of snake_case
  from_target/to_target, which a model could otherwise emit and have
  rejected by the discriminated union.
- Parse the workspace root doc's page index once per doc_links_update
  invocation (buildPageIndexFromRoot) instead of re-parsing the whole
  Yjs binary per op via readPageMetaFromRoot/resolveDocIdsByTitle,
  cutting an N-op batch from up to 2N Y.Doc parses to 1.
- Add tests proving doc_properties_read and doc_links_read convert a
  reader throw (not just a null return) into a toolError, mirroring
  database-tools.spec.ts's delegate-throw coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a getDoc call counter to the fake DocReader and cover the
buildPageIndexFromRoot single-parse refactor with a multi-op batch
(create_link/remove_link/retarget_link, mixing by-id and by-title
targets) plus an ambiguous-title case, both asserting a single root-doc
fetch regardless of op count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export the four doc-properties/doc-links tool factories from
plugins/copilot/tools/index.ts, add docPropertiesRead/docPropertiesUpdate/
docLinksRead/docLinksUpdate to PromptToolsSchema, and register their
handlers in ToolRuntime.getTools (write tools gated behind
env.dev || env.namespaces.canary, matching the database write tools;
read tools ungated).

DocPropertiesReader/DocPropertiesWriter/DocLinksWriter are provided and
exported from core/doc's DocStorageModule, mirroring DatabaseWriter.
DocLinksReader is instead provided and exported from plugins/indexer's
IndexerModule: it depends on IndexerService, and importing it from
core/doc/index.ts would close a static import cycle back through
core/index.ts -> core/auth -> core/doc (verified via oxlint's
import/no-cycle rule, which is clean both before and after this change).

Updates the two existing ToolRuntime unit tests that construct it with
positional constructor args for the 4 added DI parameters.

Marks openspec task group 8 (8.1-8.3, 8.5) complete; 8.4 (built-in.json)
is a separate commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rompt

Add docPropertiesRead/docPropertiesUpdate/docLinksRead/docLinksUpdate to
the "Chat With AFFiNE AI" prompt's config.tools, alongside the existing
database tools. This is a JSON-only edit; the native crate embeds this
prompt at build time, so it takes effect after CI's native rebuild (not
run locally as part of this task, per the task brief).

Marks openspec task 8.4 complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DocPropertiesWriter's set_title op only wrote the denormalized root
meta.pages[].title, leaving the doc's own affine:page prop:title stale.
Delegate to DocWriter.updateDocMeta (already used by doc_update_meta),
which writes both the doc-body title (native updateDocTitle) and the
root meta copy (native updateRootDocMetaTitle) via one canonical path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zebster-cmd
zebster-cmd merged commit 3333a38 into requesty-provider-spec Jul 15, 2026
29 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant