You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(server): idempotent fileRead({ifExists: true}) for race-prone batch reads
Two pathologies were silently logging 404s in DevTools and routing through
installFetchProxy's retry path on every space switch and config-load:
1. Batch readers in spaces/storage.js (readSpace, parseWidgetFiles batch,
listSpaces) walk the path index and then read all matched files. A
widget or manifest can be deleted between the index walk and the read
(concurrent space deletion, file_explorer rename, watchdog catching
up), failing the entire batch.
2. Optional config readers across the codebase load files like
~/conf/dashboard.yaml, ~/conf/onscreen-agent.yaml,
~/conf/personality.system.include.md, ~/user.yaml. On a fresh user
account these never existed, so the load wraps the call in
try/catch + isMissingFileError and treats 404 as "use defaults". The
404 is suppressed in JS but still logged by the browser before
JavaScript can intercept it.
This PR adds an opt-in `ifExists: true` option to fileRead, mirroring the
shape of fileDelete({ifExists: true}) (PR #35). Server returns 200; missing
paths appear under `skipped[]`; the singular form returns
{content: null, encoding: null, path: null, skipped: [requested]}. Strict
semantics remain the default for user-initiated reads and known-must-exist
paths so a real "this resource is gone" diagnostic still surfaces.
Server side:
- normalizeReadRequests reads options.ifExists; missing paths go into
skipped[] instead of throwing 404. Other resolution failures (empty
path, directory-instead-of-file, public-only, permission) still throw.
- readAppFiles surfaces skipped on the result envelope when non-empty
and additionally catches ENOENT from fs.readFileSync when ifExists is
set, closing the race where the path index says the file is there
but it has been removed externally since the last scan.
- readAppFile keeps its singular contract; returns content: null /
encoding: null / path: null when the only path was skipped.
- file_read endpoint reads ifExists from POST body or GET query
(?ifExists=1) so the bare-path GET form keeps working without
forcing every idempotent read into POST.
Client side:
- createFileReadRequest accepts a third positional argument for the
bare-path/array forms and an ifExists field on the object forms.
Strict bodies/queries byte-identical to today.
- fileRead branches: idempotent reads bypass the file-read batching
queue and call directly into call("file_read", ...). Mixing strict
and idempotent modes in one batch would change the strict caller's
behaviour, so the bypass keeps each idempotent call's semantic
isolated. The queue's batching benefit is preserved for the strict
default.
Migrated callers:
Pathology 1 (race-prone batch reads in spaces/storage.js):
- readSpace(...) — manifest + widgets batch
- parseWidgetFiles(...) batch — widget batch
- listSpaces(...) bulk — manifest+widgets across all spaces
Pathology 2 (optional config readers; previously try/catch + 404):
- agent/storage.js loadAgentPersonality
- user/storage.js readUserConfig
- dashboard_welcome/dashboard-prefs.js loadDashboardPrefs
- onscreen_agent/storage.js loadOnscreenAgentConfig + loadOnscreenAgentHistory
- admin/views/agent/storage.js loadAdminChatConfig + loadAdminChatHistory
- login_hooks/login-hooks.js hasFirstLoginMarker (fileRead fallback path)
- panels/panel-index.js listPanels batch
Each of these had its own copy of the isMissingFileError(...) helper for
the same regex. Where the helper has no remaining users in a file, this
PR removes it; module_remove and other strict callers keep their copies.
server/api/AGENTS.md documents the new option alongside the existing
file_write operation-modes documentation.
Strict callers untouched: readWidgetFile, readManifestFile's duplicateSpace
caller, all single-file reads paired with a known-existing target.
0 commit comments