* fix(desktop): release a capture when its start is cancelled after launch Cancelling start_build_capture during the opened app's screenshot left the capture running, so the next start was refused. The capture is now kept for the owner's cleanup, and the next start stops it first. * fix(desktop): keep a published witness error and tag an app data refusal A second failure to close a published witness returns the original error, so its record and cleanup lease stay bound. A reset the app itself refuses is an AppDataRefusal; only a reply that is not that refusal stays unsettled. A missing child of a cut view is undecided with the same description in both evaluators, and the shared cases cover it. * fix(desktop): align a cut view's missing text with the Python check A missing child of a truncated view is undecided for the same sentence in both evaluators, including the pointer the view does not contain. * fix(desktop): keep one published witness cleanup across retries A published record and its bound cleanup error stay together. A later close failure returns that error, and the unused path that built a fresh adapter error is gone. * fix(desktop): keep a bundle task read inside the witness result A cancelled, refused, or failed read of the bundle's task files returns the witness's Result, including a cleanup lease when a handle would not close. * test(desktop): pass the settle timing when reading a witness bundle The witness takes the settle timing before the bundle path, so the bundle-read tests keep the path in its own argument. * fix(desktop): bound a guest file answer and split a long controller argument A data/get whose base64 answer would exceed a quarter of the V8 heap is refused before it is read. Controller JSON that exceeds Linux's single-argument limit travels as several arguments the controller joins. * fix(desktop): keep an underreported guest file from raising its read A data/get answer is admitted only when its base64 body fits in the heap quarter after the copies held during parse. Decoded bytes larger than the computer reported are dropped. * fix(desktop): grade shipped seeds and keep selected settings and sqlite logs Seed checks follow the files the bundle ships, so an omitted lock is absent. An incomplete checkpoint is not reported as no change. A selected link keeps its spelling, and a database's wal and journal stay supporting state. Closing the fake computer stops a detached capture. * fix(desktop): keep added-record checks off ids, dates, and joined timestamps A position-named record's fields keep their paths, so an identifier, a stamped date, and text past the limit are not suggested checks. A whole-text check does not climb into an ancestor whose text joins a timestamp. * fix(desktop): bound home fetches and keep unsettled edits readable Split home-folder reads by the guest answer budget, keep a capacity error from the capture output, and drop the superseded task-argument refusal. Grade kept text per line, leave unretained dconf unread, and reserve by-position notes for XML children. Map a linked database's sidecars through the selected spelling and stop a detached capture only when its start identity still owns the pid. * fix(desktop): describe unretained dconf as unread The key comparison treats a missing copy as unknown. The comment still said that copy counted as an empty key list. * test(desktop): keep a capture capacity error through endedCapture An output past the guest answer ceiling stays the resource constraint, with its heap recovery and non-retryable status, instead of a generic capture-exit message. * fix(desktop): keep nested save times and formatting runs out of checks A field check drops save-time leaves before it joins the field, so a nested timestamp is not part of the suggested text. A text check beside a save time uses the paragraph when the diff names a formatting run. * test(desktop): read paragraph pointers from checks that have them * test(desktop): expect a large task to export intact The 96 KiB refusal is gone. A prompt past that size reaches the computer and comes back unchanged, instead of failing before export. * fix(desktop): keep real edits that save-side heuristics were dropping Repeated unnamed fields stay content checks. A formula that still has text is not renumbered with its ExprID. One gained style attribute is the edit; a repeated fill-in stays upkeep. Phone numbers and plain clock times stay text, and a decimal is the element's whole text. An older verifier matches on the task it states, not its evaluator source. * fix(desktop): cover repeated records and keep timestamps out of checks An added record whose words are already in the seed still gets the list's text when that text fits. Readiness is a start reason, so a final check names seed locks without matching the message. Field text split by a timestamp stays separate, and a timestamp line is not offered as a guard. * fix(desktop): compare a cell's joined decimal within tolerance The whole-element check keeps numeric tolerance, so 437.80 and a long float still match 437.8, including when a run splits the digits. A different number or other text does not. Both evaluators read an element's text for that comparison. * fix(desktop): grade a number from a complete XML element only A tolerance check joins an element's text only in an XML file or an XML archive entry, and leaves that check undecided when the view is cut, so a dropped descendant cannot turn the kept runs into a passing number. A JSON object with the same shape stays the value itself. Equivalent decimals and a split run on a whole cell still match. * fix(desktop): accept an older grader only when its index helper agrees The witness still matches a bundle whose evaluator is this REA's, or the older index expression alone when no path segment is a leading-zero number. A truncated verifier, a changed grader, or that older helper beside a segment such as 01 no longer counts as the same task. * fix(desktop): require the added record and leave save times out of list checks A record a selector names is checked itself, so its words have to be in that record. When an unnamed record only repeats words the seed already has, a list that holds a save time is checked by the text around that time. A later save of the same records still passes, and a missing or rewritten record fails. * fix(desktop): judge the older index helper by pointer segments only A leading zero in a file name or a URL does not change how the older index helper reads a pointer. The witness still rejects that helper when a pointer segment is a number such as 01. * fix(desktop): do not suggest list text the seed already passes A repeated record is shown by text the seed does not hold. The seed's own text is not a check. A pointer inside a check value is not a pointer segment for the older index helper. * fix(desktop): keep a linked settings file in the folder the link points at A selected ~/.config alias no longer moves MuseScore settings to that alias. The seed keeps settings/MuseScore or Documents, which is the path the export opens the note from. * fix(desktop): split home fetches by answer size and keep a size refusal A batch of tiny files stops before each path's envelope exceeds the host answer budget. A capture output past that ceiling stays a resource error instead of becoming a generic exit. * fix(desktop): seed a selected link under the path the app uses The selected spelling wins, so ~/.config/MuseScore is not rewritten to the folder it points at and the note stays the one launched document. A database wal and journal follow that database. A launch argument is respelled the same way. Teardown signals a detached PID only when its start identity still matches. * fix(desktop): keep a witness's actions and an incomplete checkpoint A failed witness save retries the action log it first recorded. A deletion left unconfirmed between the seed and the end is not reported as no change. Windows packs npm through a shell, which can start npm.cmd. * fix(desktop): read verifier pointers from the parsed checks A brace inside a check value is not an object boundary. A leading-zero pointer after that value still rejects the older index helper. * fix(desktop): prefer the most specific home spelling and retarget launch files A selected parent directory no longer hides a settings folder walked through a link. A launch filename under that folder is replaced with the seeded file's absolute path. * fix(desktop): pack the build runtime through npm's JavaScript entry npm pack and npm ci no longer spawn npm.cmd through a shell, so a temporary directory whose path contains spaces stays one argument. * fix(desktop): spell launch settings and keep a database log exact A database log matches only that file, so a neighboring folder is not moved. A launch option and the working directory use the same home spelling as the seed. * fix(desktop): move a state database's logs with the database The write-ahead log and journal follow the spelling the database received, including a walked state link. A neighboring folder stays put. A separate --config argument uses that spelling too. * fix(desktop): keep a selected file ahead of a database-log guess A file selected as itself keeps that spelling even when its name ends in -journal. Relative launch arguments are resolved in the captured working directory before that directory moves. * fix(desktop): keep outside-home database logs and option values A selected database outside the home keeps its write-ahead log. Launch options are rewritten only when they name a captured file or settings path. * fix(desktop): follow a database mapping and own limitation text A selected database keeps its log when a parent folder moves or the file sits outside the home. Trash and seed-lock tests compare the owning notice with the paths, and launch options that are not captured paths stay as written. * fix(desktop): retarget a settings folder that holds captured files A nested profile directory is a captured path when a seeded file is inside it. An option value that only resolves under the home stays as written. * fix(desktop): keep a program word and a directory's neighbor The program after env stays the program even when a captured file sits in a folder of that name. A selected directory is not a database, so a neighboring file keeps the parent folder's spelling. * fix(desktop): record whether a selected path is a directory Capture stores the walked path's kind. A neighboring file of a selected directory keeps the parent folder's spelling even when that directory never changes and its children are not content roots. * fix(desktop): mark containing paths without a pairwise scan A recorded path marks its known ancestors as directories in one walk up the path, instead of comparing every recorded path with every other. * fix(desktop): keep a selected folder that is named like a database log A folder the caller selected stays a selected root when its name ends in -journal or -wal. A generated log beside a database file still does not. * fix(desktop): refuse a build command that exceeds the argument vector Splitting one argument still leaves the whole command under Linux's E2BIG limit. A request past that budget is refused before the process starts. * fix(desktop): remap a captured path in an env assignment The program word stays the program. A NAME=value assignment keeps its name and follows a captured path value when that path's spelling moves. * fix(desktop): keep oversized text out of draft guards A value or line longer than the draft's text bound is not copied into a guard, so one large frame cannot fill the task by itself. * fix(desktop): keep an empty env value empty An empty assignment is not a path. Resolving it named the working directory and rewrote the value when that directory's spelling moved. * fix(desktop): check build runtime export capability * fix(desktop): provide complete runtime repack command * fix(desktop): retain runtime probe failures * test(desktop): identify capture independently of schema preflight * fix(desktop): preserve document URLs and value formats * fix(desktop): bound added checks and omit noisy text guards * fix(desktop): preserve launch URLs and JSON draft values * fix(desktop): recognize captured file URL documents * fix(desktop): render captured file URLs for runtime home * fix(desktop): distinguish phone numbers from save metadata * fix(desktop): preserve launch URLs and user text * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): render captured file URLs at launch * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): retain clean text in noisy record fields * style(desktop): format the noisy-record draft files with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): move launch execution to boundary suite * fix(desktop): redact build schema failure output * refactor(desktop): normalize schema failure once * fix(desktop): omit unsafe schema parser snippets * fix(desktop): preserve draft change and selected path evidence * test(desktop): group retained dconf note regressions * fix(desktop): respect archive entry format in draft checks * test(desktop): separate unowned-file draft note cases * docs(desktop): clarify task checks before scratch export * fix(desktop): respell selected paths in file URLs * test(desktop): group captured document spelling cases * fix(desktop): bound draft preview serialization * fix(desktop): restore selected state aliases * fix(desktop): respect entry format in draft noise * test(desktop): match exported alias evidence at its JSON boundary * style: fix desktop launch test formatting * style: format native command path schema * fix(desktop): map selected external launch paths * style(desktop): format seed document regressions * test(desktop): group launch URL regressions * fix(desktop): omit redundant nested state aliases * fix(desktop): compare added free text as a whole * fix(desktop): check recompressed seeds as shipped * fix(desktop): map captured launch paths once * refactor: remove unused whole-text guard state * refactor: stop passing unused text children * test: align free-text checks with whole-note policy * fix: validate recompressed desktop archive seeds * style(desktop): format archive seed regression * fix(desktop): redact runtime schema errors * fix(desktop): normalize captured file URLs * fix(desktop): report seeded files by capture path * fix(desktop): retain startup readiness failure stage * fix(desktop): retain JSON date checks * fix(desktop): preserve archive formatting checks * fix(desktop): preserve explicitly selected journal files * fix(desktop): report build command transport limits * style(desktop): format capture review fixes --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
15 KiB
Desktop application state
rea inspect-desktop-application-state <capture-folder> reads a folder written
by desktop application capture and describes
the application's state: which observed files are the application's, what each
retained version contains as typed JSON, and how each file changed between
checkpoints. It reads only the capture folder; the application is not run
again.
rea inspect-desktop-application-state ./mousepad-capture --format json
The result is Evidence with operation inspect_desktop_application_state and
predicate type rea.desktop-application-state. Its confidence is inferred,
because roles come from heuristics; the views themselves are read from the
retained bytes, which are checked against their SHA-256 before use.
Paths and roles
The view covers every file retained at the baseline and every file that changed later. Each path records the evidence the capture holds about it:
| Reason | Meaning |
|---|---|
state_directory |
It lies in one of the application's state directories, in either spelling |
opened_by_application |
An application process held it open during a sample |
named_in_window_title |
An application window title spelled its name or ~/ path on its own |
selected_path |
It lies under a --state-path or absolute --argument of the capture |
dconf_application_keys |
It is a dconf database holding groups whose path names the application |
desktop_service |
It lies where desktop services and toolkits keep shared files, or caches |
desktop_trash |
It lies in the desktop Trash, ~/.local/share/Trash |
A folder beside a state directory whose name differs from it only in letter
case counts as that state directory, so a profile the application first
creates during the capture, such as GIMP's ~/.config/GIMP beside the
candidate ~/.config/gimp, is its state too.
The role follows from the reasons, in this order: shared_desktop for a file
in the desktop Trash, which the user deleted, whatever else names it;
application_state for application keys in dconf, or a state directory outside shared desktop
locations; document for a file named in a window title or selected by the
caller, even if the application holds it open, except that a selected file
under a hidden folder of the home, such as ~/.config/MuseScore, a dotfile
there, or a lock an application holds, such as LibreOffice's .~lock.<name>#
beside a document, is application_state, judged by the path as the caller
named it, so a file reached through ~/.config/MuseScore stays the application's
when that folder is a link to ~/settings/MuseScore; the innermost selected
path holding a file decides, so a score selected itself inside the link's
target keeps its own name and is a document (a capture written before it
recorded selected paths is judged by their physical spelling);
application_state for another
open file outside shared locations; shared_desktop for other files in shared
locations; unrelated otherwise. A window title names a file only when no
other file name character adjoins the name, so day.txt does not match
today.txt - Notes.
Application names, which select dconf groups, come from the package, the
executable, the desktop entry's id and StartupWMClass, and the window match.
The entry's display name is left out, since a generic name such as Settings
would claim other applications' groups.
Versions and views
Each path lists its versions: the baseline, then its state after each
checkpoint where it, or a database's -wal or -journal file, changed. A
checkpoint that leaves an absent file absent adds no version. A file's state
at any checkpoint is its last version recorded at or before it, which is how
the export and the
task draft read it at the seed and end
checkpoints they are given; capture.checkpoints lists the names in order.
Views hold the captured bytes as they are, the capture host's name and user
included; the export removes those from the bundle. A version records
whether the file was present, the SHA-256 of its retained bytes, and a view:
| Format | Detected by | View |
|---|---|---|
sqlite |
The SQLite format 3 header |
Every ordinary table's columns, primary key and up to 500 rows |
dconf |
The GVariant header |
Groups whose path names the application, plus the names of the other groups |
zip |
A zip local file header, PK\x03\x04, whatever the name |
Its file entries in archive order, each with its own format and view |
json |
A .json name or a leading { or [ that parses |
The parsed value, from files up to 4 MiB nested up to 500 levels |
xml |
An XML name or declaration that parses | An element tree (name, attributes, children); text kept exactly, DTDs never read |
keyfile |
Only [group], key=value and comment lines, with at least one group |
Groups of key → string value, from files up to 4 MiB |
text |
Other UTF-8 text | Lines, up to 5,000 |
binary |
NUL bytes or invalid UTF-8 | None |
A gzip file is read as the document inside it; see compressed documents.
A JSON value holding a number JavaScript would round, such as an integer above 2^53, is not read rather than shown rounded.
An XML view names elements and attributes as the document spells them, prefix
included: a saved Inkscape drawing's root is svg, in its default namespace,
with sodipodi:docname and the declarations xmlns and xmlns:sodipodi
among its attributes, and a Writer paragraph is text:p. An element's
children hold its elements and text in order, text split where a CDATA
section, comment or processing instruction falls. Whitespace-only text between
child elements is formatting, and dropped, in an element that holds no other
text, unless xml:space="preserve" applies; in mixed content, an element with
text of its own beside its children such as a paragraph split into runs, it is
kept, so <p>Meet on <s>Sunday</s> <s>evening</s></p> keeps the space between
the runs. Whitespace here is XML's: space, tab, carriage return and line feed;
a no-break space is text. The view keeps up to 5,000 nodes, comments and
processing instructions counted, nested up to 64 levels, and sets truncated
when it leaves some out.
A document that begins with a DOCTYPE naming its DTD, as AbiWord's .abw
does, reads as the same element tree, and the view records the declaration
beside root, its public identifier normalized as XML matches it:
{
"doctype": {
"name": "abiword",
"public_id": "-//ABISOURCE//DTD AWML 1.0 Strict//EN",
"system_id": "http://www.abisource.com/awml.dtd"
},
"root": { "name": "abiword", "attributes": {}, "children": [] },
"truncated": false
}
An identifier the DOCTYPE leaves out is null, and a document without one
has no doctype. The DTD is never fetched or read, so it adds no default
attributes and declares no entities. A DOCTYPE with an internal subset
(<!DOCTYPE a [ … ]>), which could declare entities, is not read as XML, nor is
a document that refers to any entity but XML's five (&, <, >,
', "); such a file is text.
Exported XML views use REA's same formatting-whitespace classification, so retained XML text and the task checks that address it stay aligned.
SQLite databases are read through REA's SQLite worker from a private copy,
with any retained -wal or -journal file staged beside it; the worker
accepts an inactive rollback journal and refuses a hot one. A version is not
read when its -wal or -journal file existed but was not retained, or when
the capture did not record whether either existed, since the database alone
could misstate its committed rows. The capture records them under its content
roots, which include both beside a database selected on its own, when its
baseline walk listed every directory, read every path, entered no other
device, did not stop early and recorded what it left out, and wherever they change; an excluded log, and any elsewhere, is
unknown, since an unchanged file and a missing one look alike. After an
incomplete checkpoint, a database or sidecar without an explicit
later change remains unconfirmed even if a later walk completes. REA does not
combine that older file with a newly recorded member of the SQLite set. The
historical versions remain listed; the affected SQLite view has a view_error.
A baseline non-file at a sidecar path, or a non-directory ancestor, cannot
establish an absent file. Captures without the required baseline non-file
inventory must be made again before inspection.
A table
that cannot be read keeps its error in place of its rows, and changes to that
database are marked incomplete. dconf
databases are read with dconf dump / against a private file profile, so the
live database and the dconf service are not touched; this needs dconf-cli, or
REA_DCONF_COMMAND.
A version has a view_error when its bytes were not retained, are over the 64
MiB read limit, are missing or no longer match their digest, exceed a format's
limit above, or its reader failed.
Archives
Many applications save a document as a zip archive of XML and other files:
FreeCAD's .FCStd and .FCBak, OpenDocument (.odt, .ods, whose
content.xml is the document), Office Open XML, EPUB and JAR. Such a file is
zip, and its view lists its file entries in archive order, leaving out
directories: an entry whose name ends in /, or whose attributes mark a Unix
directory or, from an MS-DOS host, the directory attribute:
{
"entries": [
{
"name": "Document.xml",
"size": 11923,
"sha256": "…",
"format": "xml",
"view": {
"root": { "name": "Document", "attributes": {}, "children": [] },
"truncated": false
},
"view_error": null
}
],
"entry_count": 15,
"truncated": false
}
Each entry's bytes are read from the archive's verified bytes in memory,
checked against its CRC-32 and declared size, and given a format and view
by the rules and limits above, with the entry's name in place of a file name.
Nothing is extracted to disk. An entry has a view_error, with sha256,
format and view null, when it is encrypted, compressed other than stored or
deflated, larger than 16 MiB, more than 100 times its compressed size, past the
64 MiB read from one archive, or its data is damaged. A SQLite or dconf
database, or an archive, inside an archive keeps its format and sha256 and
has a view_error instead of a view: it is not read. The view lists at most
1,000 entries; entry_count counts them all, and truncated is true when some
are not listed. REA reads the central directory itself, a record at a time,
through the Zip64 end record whenever a Zip64 locator precedes the end record,
as Python's zipfile does, and hands the zip reader only the records it lists,
so an archive of hundreds of thousands of entries costs no more to read
than one of a thousand. An archive whose central directory cannot be read has a
view_error of its own.
Compressed documents
Gnumeric's .gnumeric, Dia's .dia, Xournal++'s .xopp, GnuCash's default
book and Inkscape's .svgz are XML compressed with gzip. A file that starts
with gzip's magic bytes, 1f 8b 08, whatever its name, is read as the
document inside it: its members joined, then given a format and view by the
rules above under the file's own name, so a pointer or check into it is
written exactly as for the same document uncompressed, and the task draft is
the same. The version records the compression beside the format:
{
"format": "xml",
"compression": "gzip",
"view": {
"root": { "name": "gnm:Workbook", "attributes": {}, "children": [] },
"truncated": false
},
"view_error": null
}
The document is read only when it is at most 16 MiB and at most 100 times the
file's size, an archive entry's limits, so decompressing stops there; past
them, or when the stream is damaged or cut short, the version has format
null and a view_error, and a check into it is undecided. A SQLite or dconf
database inside keeps its format and has a view_error, an archive inside is
read as an archive, and another gzip stream inside is binary. A gzip file
inside an archive reads as binary. Other compression, such as xz, Zstandard
or bzip2, is not read: such a file is binary.
Kinds of change
Between consecutive versions a change is created, deleted, or a
format-specific kind with what was added, removed and changed (up to 100 of
each, with counts):
linesfor text: lines added and removed, as a multiset.valuesfor JSON and XML: JSON pointers to values, containers included, so an array that becomes an object with the same members is changed. A pointer longer than 4096 characters is counted but not listed.valuesfor zip archives too, with array members named by selectors rather than positions wherever a selector names one member in both versions: entries by name, as/entries/[name=Document.xml], and XML elements by tag and an attribute, as/view/root/children/[name=Object][@name=Hole], so an object inserted before another does not shift it. A comparison is incomplete when either view leaves out entries, or an entry's view is missing or truncated.keysfor key files and dconf:group/key, with~and/in the key written~0and~1as in a JSON pointer, so[a/b]cand[a]b/cstay apart.rowsfor SQLite:table:and the row's primary key, or the whole row when the table has none.contentfor binary files, andunreadablewhen either version has no view.