Skip to content

pdf service v2: open documents for PDF Tools v2 - #472

Merged
ymote merged 4 commits into
mainfrom
feat/pdf-service-v2
Oct 11, 2026
Merged

ymote merged 4 commits into
mainfrom
feat/pdf-service-v2

Conversation

@ymote

@ymote ymote commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

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.md on design/pdftools-v2, 6612c90). It needs:

  • open documents whose edits and undo history last across calls;
  • page renders at screen resolution;
  • find with match rectangles;
  • comments, Fill & Sign and form fields;
  • page operations and text edits;
  • undo, redo and save.

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.open keeps 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.
  • Another app's or another scope's handle gets unknown_doc:, the same answer as a handle that never existed.
  • A caller can keep at most 8 open (too_many_open: on the 9th). The service holds at most 32 across callers.

Thread and state holder

  • Every call reaches the service on the UI thread:
    • App Hub's services::dispatch runs inline from the card runner's services::pump, while holding the registry lock;
    • the agent relay and components' host calls run from 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).
  • So the table is a thread_local and 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.close releases a document, and so does its app closing.
  • A document is bound to the opening isolate (Replier::isolate_key()) and to its storage scope (makepad's splash_storage::storage_for_heap for that key, UI-thread state too).
  • The isolate closing: the service registers a listener on App Hub's new services::on_isolate_closed (services: tell services when an isolate closes OctoSense-App-Hub#204) from register(). cancel_heap calls it with the closed isolate's heap key. Every host calls cancel_heap for 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_mut fails), the key waits in a queue for the next call's sweep.
  • The scope changing or going: every call still sweeps for a changed storage scope (another account's storage).
  • A caller without an isolate (a component's host call) gets a 15-minute idle bound. Documents an open app holds have no idle bound, so idle unsaved edits are never dropped.

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, merge with {path, pages} ranges, split, export (images, text).
  • Engine arguments are always built by the service from reviewed values, never forwarded. Every v2 method refuses a key it does not take (args::only).

Caps

  • 24–300 dpi, and at most 16 megapixels per render (checked from the page's displayed size before rendering).
  • find returns at most 500 matches.
  • At most 512 pages named per call.
  • An image export takes at most 64 pages and 256 megapixels in total, each page at most 16 MP. These are checked before anything is written.
  • The render cache in .cache/pages/<doc>/<page>@<dpi>.png holds at most 16 MiB and 64 files, oldest first. It is cleared before any write would fail with storage_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: adds Area::credit, which counts bytes a call freed outside its own writes (the cleared cache) as room again, also through a Stage.

Docs: the pdf service's section in apps/AGENTS.md, and ADR 0013's pdf row in English and Chinese.

tools.json and 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 in src/reading.rs, src/review.rs and src/change.rs.

Method Engine command(s) Class Review decision
every session js_enabled {enabled: false} code Only ever called with false, and checked off (session().javascript()) before any file opens.
pdf.open; info/text {path} doc_open {path}, doc_info code, safe path is 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. password is refused as an unknown key; protected files answer protected:. Proven by a_forms_own_scripts_never_run and a_documents_own_scripts_never_run.
pdf.close, pdf.state doc_list safe Read only.
pdf.info {doc}, pdf.text {doc} doc_info, text_extract safe At most 512 pages.
pdf.page page_render {page, dpi} safe dpi and the 16 MP cap are checked before rendering. The PNG is written by the service into its own cache.
pdf.find text_find {query, limit}, text_extract {pages} safe Returns at most 500 matches; total counts up to 10,000.
pdf.lines text_lines, text_paragraphs safe Read only.
pdf.comments comment_list safe Read only.
pdf.comment add comment_add {page, type, quads | at | rect, color, contents, author} file The attachment type reads a path. The service allows only highlight, underline, strikeout, note and textbox, and only these keys. It builds quads from the caller's rects. path, file, image, stamp, find, quads and icon are refused before the engine runs, so the command acts as a plain edit (comment_add_takes_no_file_parameter).
pdf.comment edit/delete/reply/status comment_edit, comment_delete, comment_reply, comment_set_status, comment_list safe status must be one of the five values.
pdf.fields form_fields safe Read only.
pdf.fill form_fields, form_fill {values} code A read-only field refuses the whole call as invalid: before the engine sees it. JavaScript is off, so the engine takes the NoScripts path: 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_sign fill_sign_add {page, type, at, text} safe kind must be text, date, initials, check or cross.
pdf.pages rotate/delete/move/duplicate page_rotate, page_delete, page_move, page_duplicate, doc_info safe At most 512 pages; deleting every page is refused.
pdf.pages insert_file page_insert_file {path, at, pages} file A read only: path is contained, at most 128 MiB, and checked by the engine root.
pdf.pages extract page_extract {pages, out, open: false} file out is a staging path inside the storage, committed create-only. delete, separate and out_dir are never passed.
pdf.edit_text text_edit {page, line | paragraph, text} safe Text only: no font, size or move parameters.
pdf.undo, pdf.redo edit_undo, edit_redo, doc_info safe —
pdf.save doc_save {path, full} file Writes to a staging path only. Without path it does an incremental save that replaces only the document's own file, and only from the foreground. With path it 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.merge doc_combine {paths, pages, out, open: false} file As before, plus validated page ranges.
pdf.split doc_split file As before.
pdf.export images doc_export_images {folder, dpi, pages, format: "png"} file Staging folder only; create-only commit, all or nothing. 24–300 dpi; at most 64 pages and 256 MP in all, each page at most 16 MP, all checked (too_large:) before anything is written.
pdf.export text doc_export_text {path, pages} file Staging path only; create-only commit.

Follow-ups (second commit, baed185)

The coordinator accepted all 11 contract changes below, and asked for two more:

  • Export caps: pdf.export images takes at most 64 pages and 256 megapixels in total per call, each page at most 16 MP. Over-cap calls answer too_large: before anything is written.
    • Tested at each cap and one over: 64 pages export and 65 are refused (every page, 65 named, or one page named 65 times). Sixteen 16-MP pages at 72 dpi, exactly 256,000,000 pixels, export; adding a 1-pt page, one pixel over, is refused, and so is a page of 4001 × 4000 pt. Nothing is written by a refusal, and no staging folder is left.
  • read_only on fields: pdf.fields returns read_only per field, and pdf.fill naming a read-only field answers invalid: and fills nothing, not even the other fields in the same call.
    • Tested with a read-only field (/Ff 1) in the scripted form.

Follow-ups (third commit, after the coordinator's review)

  • Stale documents from heap-key reuse:

    • App Hub gets 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 root Cargo.toml (5 crates), native-apps.json, and Cargo.lock, where only the five App Hub source lines change.
    • The pdf service registers its listener once, from register().
    • The "known gap" text is gone from docs.rs, apps/AGENTS.md and this description.
    • Permalinks to App Hub source at 8347a489 in crates/toolbox/README.md and docs/ai-services*.md stay: they cite code verified at that revision, not the current pin.
    • New test, with a real isolate: a_closed_isolates_documents_go_before_its_key_is_reused. It opens 3 documents with renders and checks that another key's cancel_heap leaves them. After cancel_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 is too_many_open:, and the old handles are unknown_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:

    • open, edit, save, edit, save (each save appends to the last), close, reopen: both edits are there;
    • save-as, edit, incremental save onto the new file, reopen: all three edits; the original keeps its two;
    • after every save: a page never drawn before renders, find returns the page with rects, and its text comes out.

    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)

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.py from the host's source hub.

On head cbdc6c7 (rebased; App Hub at merged main 80a64b45):

  • cargo test --locked -p octosense-pdf-service: 39 unit tests, plus first_call, skill and doc tests. One timing test is ignored by default.
  • cargo test --locked -p octosense-engine-area: 24 passed.
  • On the Mac: python3 tools/setup.py --update (no further changes), --check --cargo, and python3 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, plus first_call, skill and doc tests. One timing test is ignored by default.
  • cargo test --locked -p octosense-engine-area: 24 passed.
  • From 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.
  • On the Mac: python3 tools/setup.py --update (no further changes), --check --cargo, and python3 tools/native_apps.py --check.

On head baed185:

  • cargo test --locked -p octosense-pdf-service: 37 unit tests, plus first_call, skill and 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, plus first_call, skill and doc tests.
  • cargo test --locked -p octosense-engine-area: 24 tests, including the new credited_bytes_are_room_again.
  • From phone/: cargo test --locked --features mobile-apps,craft-engines -p octosense-shell host_tools: 108 passed, including every_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.
  • On the Mac: python3 tools/setup.py --check --cargo, and rom/ test_no_local_paths.

What the new tests (src/doc_tests.rs) cover:

  • every method's happy path, on the pdftools_fixture example's PDFs (included as a module) and on PDFs built in the test;
  • unknown and foreign handles (another app, another scope, malformed or closed), across every method;
  • the 9th open, with the cap per caller;
  • path escapes (.., absolute, ./, a link out) for open, insert, extract, save, both exports and merge;
  • the dpi and 16 MP caps, including at the export;
  • a password-protected PDF (made by the engine's doc_protect) answering protected: from open, info, text, merge and insert;
  • the scripted form described above;
  • comment_add refusing file and other unreviewed parameters, and the attachment type;
  • undo and redo across calls, including a re-render after an undo;
  • a full save never replacing a file: also extract, export, all-or-nothing, and a background surface's save over the document's own file;
  • the render cache's file cap and byte cap (oldest first), its clearing before storage_full:, and its refusal to follow a link;
  • release when the app's storage scope goes away, with a real makepad isolate (sandbox dir and host tag), including a changed scope;
  • the idle bound for documents no isolate holds;
  • snippet and date formatting;
  • every v2 method refusing keys it does not take.

Release timings from the ignored test (Linux host, 72-core x86_64, release build; sample PDFs are vector text):

Call Time
open, 4 pages 6.9 ms
page at 96 dpi 25–29 ms
same page, cached 0.0 ms
page at 150 dpi 53 ms
thumbnail at 36 dpi 5.3 ms
find, first call 4.5 ms
comment, fill, fill_sign, rotate, edit_text, undo, redo, save 0.5–2.3 ms each
open, 112 pages 4.7 ms
its first page 21 ms
find across 112 pages 20.6 ms

Contract changes (accepted by the coordinator; the app agent is writing them into SERVICE.md)

  1. Render cache file cap. The cache needs "at most 16 MiB and 64 files". An app's storage holds at most 256 entries for its own fs writes and files.import (makepad splash_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).
  2. Image export and the entry limit. pdf.export images makes 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).
  3. doc is a string: lowercase hex, unique per process and run. It names the render folder.
  4. Every rectangle is [x, y, w, h] (points, top-left origin), as pdf.find says: also pdf.lines box, pdf.comments rects, pdf.fields rect, and pdf.comment add rects input. That way find matches and line boxes can be passed straight into a highlight.
  5. pdf.open fields and comments are counts: form fields, and comment threads.
  6. Comment details:
    • date is ISO 8601 local time without seconds or zone (2026-10-10T22:21), because the engine's listing drops both.
    • rects holds one bounding rectangle per comment; the engine lists no quads.
    • id is 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.
    • Fill & Sign marks appear in pdf.comments, since they are annotations.
    • With no author, the engine writes "PdfCraft", so the app should always pass the person's name.
  7. pdf.save {path} re-points the document to the new file (Save As), as the engine's doc_save does. An incremental save needs the app in the foreground; from a background surface it answers invalid:.
  8. pdf.find total counts at most 10,000 matches.
  9. dpi and renders:
    • A fractional dpi is rounded; out of range is invalid:, not clamped.
    • 16 MP means 16,000,000 pixels, and it also applies to each exported page.
    • A render whose PNG is larger than 16 MiB is too_large:.
  10. Error codes for cases the contract doesn't name: a path outside storage, a taken output name, nothing to undo or redo, and an edit on a read-only document all answer invalid:.
  11. Small additions to state:
    • pdf.export text also takes pages?, and a document over 512 pages needs it.
    • Merge ranges accept "all", an empty string, and en or em dashes.
    • rotate also accepts -180.
    • pdf.fields options is always [{value, label}]. read_only per field was added (follow-ups).

Not verified

  • Not run in a shell with the PDF Tools v2 app, which is being built separately. The isolate binding was tested with a real makepad isolate inside the test process, not through the shell's card runner end to end.
  • Tests ran on the Linux build host only, not on macOS. The desktop binary (cargo check -p octosense) and the shell without craft-engines weren't built.
  • The shell host_tools suite (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.
  • An export at the 256-megapixel cap still holds the UI thread for a few seconds until Engine host services run inline on the UI thread under the SERVICES lock #399.

🤖 Generated with Claude Code

ymote and others added 4 commits October 10, 2026 16:39
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
ymote force-pushed the feat/pdf-service-v2 branch from cbdc6c7 to c0353fa Compare October 10, 2026 23:39
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
ymote marked this pull request as ready for review October 11, 2026 00:09
@ymote

ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on c0353fa (tools/ci-local.sh --only all --linux-host --offload, 29m01s, Darwin arm64; the ubuntu jobs and the macOS jobs' portable steps (their macOS steps ran here) on the Linux build host, Linux x86_64).

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.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop (linux) Prepare the pinned framework sources PASS 0s
desktop.yml / desktop (linux) One copy of every shell source PASS -
desktop.yml / desktop (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
desktop.yml / desktop (linux) The native apps' generated places match native-apps.json PASS 0s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 14m50s
desktop.yml / desktop Desktop tools and scripts PASS 24s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 1s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 8m21s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 5s
phone.yml / home (linux) Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home (linux) Verify the single runtime graph PASS 2s
phone.yml / home (linux) The native apps' generated places match native-apps.json PASS 0s
phone.yml / home (linux) The pinned octos revision PASS -
phone.yml / home (linux) Build octos (unless cached) PASS -
phone.yml / home (linux) Build Home, App Hub admission and runtime policy tests PASS 3m14s
phone.yml / home (linux) Test Home, App Hub admission and runtime policy PASS 40s
phone.yml / home (linux) Two-lane scenario (real kernel, scripted model) PASS 27s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m47s
phone.yml / home (linux) Test opt-in AppCard PASS 3s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 2m51s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 4s
phone.yml / home (linux) Build linked Terminal tests PASS 2m25s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m49s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 3s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 7s
phone.yml / home Compile Home and its bundled modules PASS 5m08s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Shared system app interface PASS 0s
apps.yml / services (linux) Kernel service PASS 16s
apps.yml / services (linux) App peers PASS 29s
apps.yml / services (linux) AI providers config PASS 4s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 47s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 9s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m30s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS -
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 37s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m00s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m33s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 2m56s
apps.yml / apps (linux) AppCard runtime setup safeguards PASS 0s
apps.yml / apps (linux) AppCard clippy (own crates; shared runtime compiles) PASS 28s
apps.yml / apps (linux) AppCard transport and store PASS 8s
apps.yml / apps (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 4s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m42s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

Native GitHub acceptance passed on the same head: embedded-browser.yml.

@ymote
ymote merged commit 3bd15ba into main Oct 11, 2026
5 of 9 checks passed
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>
@ymote
ymote deleted the feat/pdf-service-v2 branch October 11, 2026 00:11
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.

1 participant