files implementation
Cross-target transfers
lib/transfer.mjs defines the TransferFile tool; index.mjs registers it. The tool delegates admission and byte movement to
ctx.host.projectFiles().transfer, restricted by core to Files with reads:['project-files'].
Core reuses Sandbox local/project operations and the shared guestFiles stream/upload client.
Both endpoints are {kind:'project',projectId,path}, including paired computers; no endpoint names a
selected computer. A computer endpoint uses a daemon binding, native path resolution and member-only
canExecute admission, including for administrators. The default permission is ask. Source reads and optional
source removal use the same observed version. A destination is create-only unless its explicit
replacement version is supplied. Source removal follows confirmed commit, and cancellation,
changed files or an unknown commit never trigger a second attempt. Progress uses onUpdate
with acknowledged destination bytes. No whole-file buffer or persisted job is created.
The shared local path resolver also serves ordinary local Files operations.
Host Glob traversal
lib/search.mjs::walkFiles(root, limit) returns a promise for { files, unreadableDirectories }.
Glob awaits the sequential depth-first walk, preserving directory entry order, ignored directories and
the existing rule that descendant symlinks are not followed. A root symlink retains the existing
stat behavior. Glob requests WALK_CAP + 1 to distinguish an exact-cap tree from an incomplete walk.
A descendant EACCES skips that directory and records its path; Glob marks details.walkIncomplete,
includes details.unreadableDirectories and displays an incompleteness notice. Root permission failures
and all other I/O errors propagate through the existing tool error path. Directory reads yield through
filesystem promises; traversal remains sequential and has no new cache, timeout or cap. Each directory
listing is still materialized as a whole. Glob's matching, modification-time sort and result shaping keep
their existing main-loop cost after the walk.
PDF reads and target versions
Read uses the opening guest-file block only for classification, then runs Poppler through
managed.mjs::exec on the managed guest or paired computer. The PDF is not downloaded for hashing.
The opening content-backed version and a fresh stat after conversion must agree. Only a fully visible
render records markFileVersionRead and emits details.fileVersion; selected pages, truncation and
omitted images do not authorize mutation. Host PDFs retain their whole-file SHA-256 baseline.
readState.mjs stores either a hash or a target version in its existing bounded map. The spawn replay
hook accepts a version only with a local or managed projectRef, using the same target/path key as the
live read. Write and Edit pass the newly read target version to readGuardError, then retain the
provider's compare-and-swap write. Another computer or a host path cannot reuse that authorization.
Concurrent changes during conversion, after Read or between the guard and CAS are refused.
No new persistent state or transfer protocol is introduced.
documents.mjs reports missing converters at all PDF stages with the target OS's install command.
Local execution loss keeps LOCAL_EXECUTION_LOST; cancellation and permission errors retain their
typed failure metadata, and the adapter cancels and releases the execution lease. Conversion never
falls back to a server process. Local and managed Glob errors use their own cwd and never call the host
sibling-suggestion reader.
File mutation serialization
File mutations: Write and Edit share the file-local queuedMutation entry. Host mutations serialize through
withFileMutationQueue and recheck path authority inside the queue. Managed mutations use provider CAS without a
guest-local queue; read-before-write guards and result details remain tool-owned.
Diff computations
Registration binds lib/diff.mjs through ctx.host.computation with the input-bounded local option.
It calls only lib/editCore.mjs::displayDiff/unifiedPatch, once each in one job; Edit planning and
all existing 200-line display and 800-line patch rules stay in editCore.
The wrapper returns [diff] when the original patch is undefined and [diff, patch] otherwise.
Write, ordinary Edit and create-only Edit use this one binding in both host and managed execution.
Core owns the local/worker decision, private async string transport and lifecycle cancellation.
The read guard, write and read-state update complete before projection. The host file queue remains held until the diff result settles, so another mutation of the same file cannot overtake it; other files remain independent. Post-write projection does not receive the tool signal: an already aborted call, abort while waiting in the host queue, abort at the write or during diff still returns the original successful result after a completed mutation. Queue admission and mutation cancellation remain unchanged. Plugin lifecycle stop still cancels the computation. A genuine worker error after writing never retries or falls back to synchronous work; it reports that the file was written and asks the caller to read current contents before another mutation. Success text, hashes, file-access metadata and optional diff/patch fields retain their original shapes. No new Write size guard or projection timeout is imposed.
File access metadata
A Read, Write or Edit result that shows or changes a file carries details.fileAccess.path, the absolute path that core reads to activate a .elowen/rules instruction module (src/brain/session/instructionActivation.ts). Refusals, errors, reads past the end of a file and a PDF render that is not fully visible carry no fileAccess, so no module loads from a file the model did not see; an empty file read still carries it.
Internal ownership
This section is for maintainers of the plugin. It describes which module owns each behavior; end users do not need it.
index.mjs registers the tools and the session-spawn replay hook. It owns path authorization, host/managed selection, result path metadata and mutation ordering. Helpers import their concrete dependencies directly; they do not register tools or widen access.
lib/editCore.mjsowns exact and opt-in fuzzy edit planning, BOM/line-ending preservation, and the existing display-diff and unified-patch caps.lib/diff.mjsis the computation entry bound throughctx.host.computation: one job renders the display diff and the unified patch that Write and Edit attach to their result.lib/transfer.mjsowns theTransferFiletool definition; admission, streaming and the move commit belong to core's project-file seam.lib/documents.mjsowns PDF page selection, bounded Poppler execution and notebook rendering. The Read handler supplies the resolved read cap, PDF page cap and image support, then decides whether the visible result authorizes mutation.lib/readState.mjsowns the single bounded session/file map, the classification probereadFileProbe, whole-file SHA-256 or target-version baselines, range deduplication, history replay, and streamed text snapshots. Read, Write, Edit and the spawn hook use this same module instance. Replay retains distinct host paths,managed:<projectId>\0<path>keys andlocal:<projectId>\0<path>keys, the last one case-folded on Windows; missing sessions keep the existing inert guard behavior. State is capped at 64 sessions and 512 files per session. Host text reads retain the selected page while hashing and counting the complete file through sequential asynchronous 64 KiB file-handle reads, allowing the event loop to serve other work between chunks. Even a small page still scans the whole file to preserve the SHA-256 mutation baseline and total line count. The same open handle is checked with bigint stats before and after the scan; changed device, inode, size or nanosecond modification/change times reject the snapshot. The classification probe must also match, and the handle closes on success and on errors before Read can record authorization. Managed reads consumereadGuestStreamfromelowen-plugin-shared/guestFilesover their authorized provider binding. A complete opening read is the same snapshot's final page, so a file fitting one 128 KiB block costs exactly one guest read, including classification and text formatting. Larger snapshots pin subsequent pages to the opening version; the final read rehashes and needs no closing stat. A later write still compares against a freshly hashed CAS version.lib/search.mjsowns Glob matching/traversal and the sharedrunRgexecutor for Search, Grep and--type-list, including stable paging, the host type-name cache and transport-failure classification. It preserves the five-second timeout, 500-column result bound and shared traversal cap. Missing-path messages stay with the tool handlers.lib/imageFormats.mjsowns the frozen magic-byte parser and inline image limits shared by Read and document rendering.lib/toolResults.mjsowns Files' result metadata wrappers overelowen-plugin-shared/toolResult, preserving successful results, actionable refusals and error shapes.
managed.mjs owns guest path resolution, live provider binding, cancellation, stored-output integrity routing and guest command execution. All file I/O sequences use elowen-plugin-shared/guestFiles: the opening read is reused by whole-file or streamed reads, small CAS writes use one operation, larger writes use shared uploads, parents are created only after parent_missing, and listing pages and walk use the same client. Tool bounds, read-before-write authorization and result wording stay with Files.
Tests import helpers from their owning modules. The plugin entry exports only register; there are no compatibility re-exports or additional public plugin seams.