Repository navigation
pdf service v2: open documents for PDF Tools v2 - #472
Merged
Merged
Conversation
ymote
force-pushed
the
feat/pdf-service-v2
branch
from
October 10, 2026 23:37
887d448 to
cbdc6c7
Compare
The pdf host service kept no state: every call opened a fresh engine session and the file, for five methods. PDF Tools v2's contract (apps/pdftools/design/SERVICE.md on design/pdftools-v2) needs open documents whose edits and undo history last across calls, page renders at screen resolution, find with match rectangles, comments, Fill & Sign, form fields, page operations, text edits, undo, redo and save. - Open documents keyed by caller app, storage scope and handle, at most 8 per caller, in a UI-thread thread_local: every call arrives on the UI thread (App Hub's pump, the relay's host_tools::pump), so no lock. Released on pdf.close and when the opening isolate's storage scope goes away (makepad's splash_storage), swept on every call because App Hub has no isolate-closed hook; callers with no isolate get a 15-minute idle bound. - Every SERVICE.md method on the engine commands reviewed for it, with arguments the service builds; every v2 method refuses keys it does not take. Writing file commands get only a staging path inside the storage; comment_add never sees its attachment type or a path; doc_open and form_fill run with JavaScript off, proven by a scripted form and the engine's scripted XFA form. - Caps: 24-300 dpi and 16 MP a render, find at most 500, 512 pages a call, a render cache of 16 MiB and 64 files (an app's storage holds 256 entries), cleared before a write would fail with storage_full; stable error codes on every method. - engine-area: Area::credit, bytes a call freed outside its own writes. - Docs: apps/AGENTS.md, ADR 0013's pdf row in both languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-ups to the accepted contract: - pdf.export images takes at most 64 pages and 256 megapixels in all, each page at most 16 MP, refused as too_large before anything is written: each page is a file against the storage's 256-entry limit, and the render work stays bounded on the UI thread until #399. - pdf.fields returns read_only per field; pdf.fill naming a read-only field answers invalid and fills nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A heap key is an address, so a reopened PDF Tools could get its predecessor's key before any pdf call saw the death: the old documents then survived the sweep and filled the cap of 8. App Hub now tells services when an isolate closes (services::on_isolate_closed, called from cancel_heap; OctoSense-App-Hub#204). The pdf service registers its listener from register(), and every document bound to a closing isolate goes then, with its renders. If a call holds the table, the key waits for the next sweep, which still releases documents whose storage scope changed. - App Hub repinned to #204's head 8c86a4b9: the root Cargo.toml, native-apps.json, and the App Hub source lines in Cargo.lock. - Tests: a real isolate's closing releases its 3 documents and their renders, and a new isolate on the same key opens all 8; work goes on after each save, incremental and save-as (renders, find, text, further saves, reopen). - Docs: the known-gap text is gone from docs.rs and apps/AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App Hub #204 (services::on_isolate_closed) is merged, and its merge commit is App Hub main. The pin moves from #204's branch head 8c86a4b9 to it: the root Cargo.toml (5 crates), native-apps.json, and the five App Hub source lines of Cargo.lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
force-pushed
the
feat/pdf-service-v2
branch
from
October 10, 2026 23:39
cbdc6c7 to
c0353fa
Compare
ymote
added a commit
that referenced
this pull request
Oct 10, 2026
The pdf service v2 (#472) settled what the draft left open: - the render cache holds at most 16 MiB and 64 files per app, since app storage allows 256 entries and imports and library saves need room; - pdf.export images: at most 64 pages and 256 megapixels a call, each page at most 16 MP; export text takes pages too; - doc is a lowercase hex string; every rectangle is [x, y, w, h] in points, and find's match rects can go straight into a highlight; - pdf.open's fields and comments are counts; - a save to a new path re-points the open document; a plain save from a background surface answers invalid:; - comments: local dates without zone or seconds, one bounding box, ids of unnamed comments @page-index, Fill & Sign marks in the list, and the person's name always passed (the engine otherwise writes "PdfCraft"); - find's total saturates at 10,000; a fractional dpi is rounded and an out of range one is invalid:, and a render over 16 MiB is too_large:; - invalid: also covers paths outside storage, taken output names, nothing to undo or redo and edits to read-only documents; - merge ranges accept "all", "" and typed dashes; rotate accepts -180; field options are {value, label}, and fields gain read_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
added a commit
that referenced
this pull request
Oct 10, 2026
The pdf service v2 (#472) settled what the draft left open: - the render cache holds at most 16 MiB and 64 files per app, since app storage allows 256 entries and imports and library saves need room; - pdf.export images: at most 64 pages and 256 megapixels a call, each page at most 16 MP; export text takes pages too; - doc is a lowercase hex string; every rectangle is [x, y, w, h] in points, and find's match rects can go straight into a highlight; - pdf.open's fields and comments are counts; - a save to a new path re-points the open document; a plain save from a background surface answers invalid:; - comments: local dates without zone or seconds, one bounding box, ids of unnamed comments @page-index, Fill & Sign marks in the list, and the person's name always passed (the engine otherwise writes "PdfCraft"); - find's total saturates at 10,000; a fractional dpi is rounded and an out of range one is invalid:, and a render over 16 MiB is too_large:; - invalid: also covers paths outside storage, taken output names, nothing to undo or redo and edits to read-only documents; - merge ranges accept "all", "" and typed dashes; rotate accepts -180; field options are {value, label}, and fields gain read_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote
marked this pull request as ready for review
October 11, 2026 00:09
Contributor
Author
|
Local CI passed on c0353fa ( GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.
Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):
Notes:
Native GitHub acceptance passed on the same head: embedded-browser.yml. |
ymote
added a commit
that referenced
this pull request
Oct 11, 2026
The pdf service v2 (#472) settled what the draft left open: - the render cache holds at most 16 MiB and 64 files per app, since app storage allows 256 entries and imports and library saves need room; - pdf.export images: at most 64 pages and 256 megapixels a call, each page at most 16 MP; export text takes pages too; - doc is a lowercase hex string; every rectangle is [x, y, w, h] in points, and find's match rects can go straight into a highlight; - pdf.open's fields and comments are counts; - a save to a new path re-points the open document; a plain save from a background surface answers invalid:; - comments: local dates without zone or seconds, one bounding box, ids of unnamed comments @page-index, Fill & Sign marks in the list, and the person's name always passed (the engine otherwise writes "PdfCraft"); - find's total saturates at 10,000; a fractional dpi is rounded and an out of range one is invalid:, and a render over 16 MiB is too_large:; - invalid: also covers paths outside storage, taken output names, nothing to undo or redo and edits to read-only documents; - merge ranges accept "all", "" and typed dashes; rotate accepts -180; field options are {value, label}, and fields gain read_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The pdf host service kept no state. Every call opened a fresh pdfcraft session and the file, and there were only five methods. PDF Tools v2 is a desktop document workspace (contract:
apps/pdftools/design/SERVICE.mdondesign/pdftools-v2, 6612c90). It needs:The pinned engine already does all of these. This PR serves them to apps' own requests. Another session is building the app against the same contract.
What
Open documents (
apps/pdf/host-service/src/docs.rs)pdf.openkeeps the engine's document under a handle. A document belongs to the caller app, its storage scope (the area's root, canonicalized) and the handle.unknown_doc:, the same answer as a handle that never existed.too_many_open:on the 9th). The service holds at most 32 across callers.Thread and state holder
services::dispatchruns inline from the card runner'sservices::pump, while holding the registry lock;host_tools::pump, also on the UI thread (Android's Mail job can drive that pump without a window, but the pdf engine is desktop only).thread_localand the UI thread takes no lock for it (makepad's rule). A call on another thread sees an empty table of its own and fails closed.Release
pdf.closereleases a document, and so does its app closing.Replier::isolate_key()) and to its storage scope (makepad'ssplash_storage::storage_for_heapfor that key, UI-thread state too).services::on_isolate_closed(services: tell services when an isolate closes OctoSense-App-Hub#204) fromregister().cancel_heapcalls it with the closed isolate's heap key. Every host callscancel_heapfor a closing isolate: App Hub's card shutdown, and the shell's glance, script-app and Wasm hosts. It runs on the UI thread, before a new isolate can reuse the key (a key is an address). Every document bound to the isolate goes then, with its renders. If a call holds the table at that moment (try_borrow_mutfails), the key waits in a queue for the next call's sweep.Every SERVICE.md method
open,close,state,info {doc},page,find,lines,text {doc},comments,comment(add, edit, delete, reply, status),fields,fill,fill_sign,pages(rotate, delete, move, duplicate, insert_file, extract),edit_text,undo,redo,save,mergewith{path, pages}ranges,split,export(images, text).args::only).Caps
findreturns at most 500 matches..cache/pages/<doc>/<page>@<dpi>.pngholds at most 16 MiB and 64 files, oldest first. It is cleared before any write would fail withstorage_full:. A link in it is refused, never followed.Errors: every method, the file methods included, now answers with a stable code (
src/codes.rs).Engine panics: a panic in the engine becomes
damaged:instead of unwinding through App Hub's registry lock. A document the panic hit is closed.octosense-engine-area: addsArea::credit, which counts bytes a call freed outside its own writes (the cleared cache) as room again, also through aStage.Docs: the pdf service's section in
apps/AGENTS.md, and ADR 0013's pdf row in English and Chinese.tools.jsonand the system agent's five pdf tools are unchanged.Safety review, per engine command
Classes are from
skill/safety.json. The review text also sits beside each method insrc/reading.rs,src/review.rsandsrc/change.rs.js_enabled {enabled: false}false, and checked off (session().javascript()) before any file opens.pdf.open;info/text{path}doc_open {path},doc_infopathis contained in the storage, at most 128 MiB, and confined again by the engine's root. JavaScript is off, so the XFA open scripts don't run.passwordis refused as an unknown key; protected files answerprotected:. Proven bya_forms_own_scripts_never_runanda_documents_own_scripts_never_run.pdf.close,pdf.statedoc_listpdf.info {doc},pdf.text {doc}doc_info,text_extractpdf.pagepage_render {page, dpi}pdf.findtext_find {query, limit},text_extract {pages}totalcounts up to 10,000.pdf.linestext_lines,text_paragraphspdf.commentscomment_listpdf.comment addcomment_add {page, type, quads | at | rect, color, contents, author}attachmenttype reads apath. The service allows onlyhighlight,underline,strikeout,noteandtextbox, and only these keys. It buildsquadsfrom the caller's rects.path,file,image,stamp,find,quadsandiconare refused before the engine runs, so the command acts as a plain edit (comment_add_takes_no_file_parameter).pdf.comment edit/delete/reply/statuscomment_edit,comment_delete,comment_reply,comment_set_status,comment_liststatusmust be one of the five values.pdf.fieldsform_fieldspdf.fillform_fields,form_fill {values}invalid:before the engine sees it. JavaScript is off, so the engine takes theNoScriptspath: values are stored as given and no XFA script runs. Acrobat's AF patterns are evaluated natively, with no interpreter. The proof form carries a keystroke, a format, a calculate and a refuse-everything validate script, a document script, and a/OpenAction. The engine's default session shows they are live (calculate runs, validate refuses, a script writes another field). Through the service none leaves a mark, through every method, and after save and reopen. The XFA form's initialize and calculate scripts stay off too.pdf.fill_signfill_sign_add {page, type, at, text}kindmust betext,date,initials,checkorcross.pdf.pagesrotate/delete/move/duplicatepage_rotate,page_delete,page_move,page_duplicate,doc_infopdf.pages insert_filepage_insert_file {path, at, pages}pathis contained, at most 128 MiB, and checked by the engine root.pdf.pages extractpage_extract {pages, out, open: false}outis a staging path inside the storage, committed create-only.delete,separateandout_dirare never passed.pdf.edit_texttext_edit {page, line | paragraph, text}pdf.undo,pdf.redoedit_undo,edit_redo,doc_infopdf.savedoc_save {path, full}pathit does an incremental save that replaces only the document's own file, and only from the foreground. Withpathit does a full save to a new file and never replaces one, foreground or not. If the commit fails after the engine has marked the document saved, an undo and a redo mark it edited again.pdf.mergedoc_combine {paths, pages, out, open: false}pdf.splitdoc_splitpdf.export imagesdoc_export_images {folder, dpi, pages, format: "png"}too_large:) before anything is written.pdf.export textdoc_export_text {path, pages}Follow-ups (second commit, baed185)
The coordinator accepted all 11 contract changes below, and asked for two more:
pdf.export imagestakes at most 64 pages and 256 megapixels in total per call, each page at most 16 MP. Over-cap calls answertoo_large:before anything is written.read_onlyon fields:pdf.fieldsreturnsread_onlyper field, andpdf.fillnaming a read-only field answersinvalid:and fills nothing, not even the other fields in the same call./Ff 1) in the scripted form.Follow-ups (third commit, after the coordinator's review)
Stale documents from heap-key reuse:
services::on_isolate_closed(services: tell services when an isolate closes OctoSense-App-Hub#204, head 8c86a4b9). App Hub is repinned to that head: the rootCargo.toml(5 crates),native-apps.json, andCargo.lock, where only the five App Hubsourcelines change.register().docs.rs,apps/AGENTS.mdand this description.crates/toolbox/README.mdanddocs/ai-services*.mdstay: they cite code verified at that revision, not the current pin.a_closed_isolates_documents_go_before_its_key_is_reused. It opens 3 documents with renders and checks that another key'scancel_heapleaves them. Aftercancel_heap(heap)they're gone at once, with their renders. A new isolate with the same key, app tag and storage then opens all 8, the 9th istoo_many_open:, and the old handles areunknown_doc:. Closing while a call holds the table queues the key, and the next call releases its documents.Saving, then continuing to work: new test
work_goes_on_after_each_save:It passes without changes. The engine re-opens the bytes it saved (
mark_saved), and the renderer and text reads use the document's in-memory bytes, so nothing reads the moved staging file.Repin and rebase (fourth commit)
main80a64b45 (Host requests settle; background surfaces cannot raise sheets #204's merge commit). It touches the same sites as before: the rootCargo.toml(5 crates),native-apps.json, and only the five App Hubsourcelines inCargo.lock. Thewindowscrates are untouched.origin/mainde32a46, with no commits squashed. Expose native D3 charts in contained apps and Glance #470 (native D3 charts) adds themakepad-d3crate from the same Octoscript-Makepad pin toCargo.tomlandCargo.lock. It moves no App Hub, makepad or Octoscript-Makepad pin, so both sides merged without conflict. Cargo accepts the merged lock with--locked.Tests
These suites aren't in CI (#412), so I ran them myself on the Linux build host, in a scratch checkout prepared by
tools/setup.pyfrom the host's source hub.On head cbdc6c7 (rebased; App Hub at merged
main80a64b45):cargo test --locked -p octosense-pdf-service: 39 unit tests, plusfirst_call,skilland doc tests. One timing test is ignored by default.cargo test --locked -p octosense-engine-area: 24 passed.python3 tools/setup.py --update(no further changes),--check --cargo, andpython3 tools/native_apps.py --check.On head 887d448 (App Hub pinned to #204's head 8c86a4b9):
cargo test --locked -p octosense-pdf-service: 39 unit tests, plusfirst_call,skilland doc tests. One timing test is ignored by default.cargo test --locked -p octosense-engine-area: 24 passed.phone/:cargo test --locked --features mobile-apps,craft-engines -p octosense-shell host_tools: 108 passed.cargo clippy --locked -p octosense-pdf-service --all-targets --no-deps: clean.python3 tools/setup.py --update(no further changes),--check --cargo, andpython3 tools/native_apps.py --check.On head baed185:
cargo test --locked -p octosense-pdf-service: 37 unit tests, plusfirst_call,skilland doc tests. One timing test is ignored by default.cargo clippy --locked -p octosense-pdf-service --all-targets --no-deps: clean.On the first head (415b0d7):
cargo test --locked -p octosense-pdf-service: 35 unit tests, plusfirst_call,skilland doc tests.cargo test --locked -p octosense-engine-area: 24 tests, including the newcredited_bytes_are_room_again.phone/:cargo test --locked --features mobile-apps,craft-engines -p octosense-shell host_tools: 108 passed, includingevery_granted_engine_tool_answers_within_its_declared_result, which drives the five pdf agent tools through the relay.cargo clippy --locked -p octosense-pdf-service -p octosense-engine-area --all-targets --no-deps: clean.python3 tools/setup.py --check --cargo, androm/test_no_local_paths.What the new tests (
src/doc_tests.rs) cover:pdftools_fixtureexample's PDFs (included as a module) and on PDFs built in the test;.., absolute,./, a link out) for open, insert, extract, save, both exports and merge;doc_protect) answeringprotected:from open, info, text, merge and insert;comment_addrefusing file and other unreviewed parameters, and the attachment type;storage_full:, and its refusal to follow a link;Release timings from the ignored test (Linux host, 72-core x86_64, release build; sample PDFs are vector text):
Contract changes (accepted by the coordinator; the app agent is writing them into SERVICE.md)
fswrites andfiles.import(makepadsplash_storage::MAX_ENTRIES), so a cache of hundreds of thumbnails would make PDF Tools' library saves and imports fail with "too many files". v1 hits the same limit (its README).pdf.export imagesmakes one entry per page, so a large export can push the storage past 256 entries the same way. Resolved by the 64-page and 256-megapixel export caps (follow-ups).docis a string: lowercase hex, unique per process and run. It names the render folder.[x, y, w, h](points, top-left origin), aspdf.findsays: alsopdf.linesbox,pdf.commentsrects,pdf.fieldsrect, andpdf.comment addrectsinput. That way find matches and line boxes can be passed straight into a highlight.pdf.openfieldsandcommentsare counts: form fields, and comment threads.dateis ISO 8601 local time without seconds or zone (2026-10-10T22:21), because the engine's listing drops both.rectsholds one bounding rectangle per comment; the engine lists no quads.idis the comment's name in the file, or@<page>-<index>for an unnamed comment. That form stays valid only until that page's comments change.pdf.comments, since they are annotations.author, the engine writes "PdfCraft", so the app should always pass the person's name.pdf.save {path}re-points the document to the new file (Save As), as the engine'sdoc_savedoes. An incremental save needs the app in the foreground; from a background surface it answersinvalid:.pdf.findtotalcounts at most 10,000 matches.invalid:, not clamped.too_large:.invalid:.pdf.export textalso takespages?, and a document over 512 pages needs it."all", an empty string, and en or em dashes.rotatealso accepts -180.pdf.fieldsoptionsis always[{value, label}].read_onlyper field was added (follow-ups).Not verified
cargo check -p octosense) and the shell withoutcraft-enginesweren't built.host_toolssuite (mobile-apps,craft-engines) last ran on 887d448, before the rebase onto Expose native D3 charts in contained apps and Glance #470 and the repin. The full local CI gate is left to the coordinator's joint gate with the UI work.🤖 Generated with Claude Code