Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions app/L0/_all/mod/_core/admin/views/agent/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ function getRuntime() {
return runtime;
}

function isMissingFileError(error) {
const message = String(error?.message || "");
return /\bstatus 404\b/u.test(message) || /File not found\./u.test(message);
}

function isSingleUserAppRuntime(runtime) {
return Boolean(runtime?.config?.get?.("SINGLE_USER_APP", false));
}
Expand Down Expand Up @@ -198,16 +193,20 @@ async function buildStoredConfigPayload(runtime, { settings, systemPrompt }) {
export async function loadAdminChatConfig() {
const runtime = getRuntime();

// Idempotent read: a fresh user has no `~/conf/admin-chat.yaml` yet.
// ifExists returns content: null instead of throwing 404.
let result;
try {
const result = await runtime.api.fileRead(config.ADMIN_CHAT_CONFIG_PATH);
return normalizeStoredConfig(runtime, runtime.utils.yaml.parse(String(result?.content || "")));
result = await runtime.api.fileRead(config.ADMIN_CHAT_CONFIG_PATH, "utf8", { ifExists: true });
} catch (error) {
if (isMissingFileError(error)) {
return createDefaultConfig();
}

throw new Error(`Unable to load admin chat config: ${error.message}`);
}

if (typeof result?.content !== "string") {
return createDefaultConfig();
}

return normalizeStoredConfig(runtime, runtime.utils.yaml.parse(result.content));
}

export async function saveAdminChatConfig(nextConfig) {
Expand All @@ -229,20 +228,27 @@ export async function saveAdminChatConfig(nextConfig) {
export async function loadAdminChatHistory() {
const runtime = getRuntime();

// Idempotent read: history file may not exist on first run.
let result;
try {
const result = await runtime.api.fileRead(config.ADMIN_CHAT_HISTORY_PATH);
const parsed = JSON.parse(String(result?.content || "[]"));
return Array.isArray(parsed) ? parsed : [];
result = await runtime.api.fileRead(config.ADMIN_CHAT_HISTORY_PATH, "utf8", { ifExists: true });
} catch (error) {
if (isMissingFileError(error)) {
return [];
}
throw new Error(`Unable to load admin chat history: ${error.message}`);
}

if (typeof result?.content !== "string") {
return [];
}

try {
const parsed = JSON.parse(result.content || "[]");
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("Unable to load admin chat history: invalid JSON.");
}

throw new Error(`Unable to load admin chat history: ${error.message}`);
throw error;
}
}

Expand Down
13 changes: 3 additions & 10 deletions app/L0/_all/mod/_core/agent/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,15 @@ function getRuntime() {
return runtime;
}

function isMissingFileError(error) {
const message = String(error?.message || "");
return /\bstatus 404\b/u.test(message) || /File not found\./u.test(message) || /Path not found\./u.test(message);
}

export async function loadAgentPersonality() {
const runtime = getRuntime();

// Idempotent read: this config is optional. Use ifExists so a missing
// file returns content: null instead of throwing 404.
try {
const result = await runtime.api.fileRead(AGENT_PERSONALITY_PATH);
const result = await runtime.api.fileRead(AGENT_PERSONALITY_PATH, "utf8", { ifExists: true });
return String(result?.content || "");
} catch (error) {
if (isMissingFileError(error)) {
return "";
}

throw new Error(`Unable to load agent personality: ${error.message}`);
}
}
Expand Down
14 changes: 4 additions & 10 deletions app/L0/_all/mod/_core/dashboard_welcome/dashboard-prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ function getRuntime() {
return runtime;
}

function isMissingFileError(error) {
const message = String(error?.message || "");
return /\bstatus 404\b/u.test(message) || /File not found\./u.test(message) || /Path not found\./u.test(message);
}

function parseStoredBoolean(value) {
if (value === true || value === false) {
return value;
Expand Down Expand Up @@ -92,14 +87,13 @@ export function subscribeDashboardWelcomeHiddenChange(callback) {
export async function loadDashboardPrefs() {
const runtime = getRuntime();

// Idempotent read: a fresh user has no `~/conf/dashboard.yaml` yet. Use
// ifExists so the missing file returns content: null instead of throwing
// 404 (and triggering DevTools console noise on every space switch).
try {
const result = await runtime.api.fileRead(DASHBOARD_CONFIG_PATH);
const result = await runtime.api.fileRead(DASHBOARD_CONFIG_PATH, "utf8", { ifExists: true });
return normalizeDashboardPrefs(runtime.utils.yaml.parse(String(result?.content || "")));
} catch (error) {
if (isMissingFileError(error)) {
return normalizeDashboardPrefs({});
}

throw new Error(`Unable to load dashboard settings: ${error.message}`);
}
}
Expand Down
43 changes: 41 additions & 2 deletions app/L0/_all/mod/_core/framework/js/api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,23 @@ function serializeStableValue(value) {
.join(",")}}`;
}

function createFileReadRequest(pathOrFiles, encoding) {
function createFileReadRequest(pathOrFiles, encoding, options) {
// `ifExists: true` opts into idempotent read semantics: missing paths
// resolve to a 200 response with `content: null` (singular form) or
// listed under `skipped` (batch form) instead of throwing 404. The
// option arrives either through the third positional argument
// (bare-path / array forms) or as an `ifExists` field on the input
// object (object forms); the input object wins when both are set.
const optionsObject = isPlainObject(options) ? options : {};
const ifExistsFlag = optionsObject.ifExists === true;
const ifExistsBody = ifExistsFlag ? { ifExists: true } : {};
const ifExistsQuery = ifExistsFlag ? { ifExists: "1" } : {};

if (Array.isArray(pathOrFiles)) {
return {
method: "POST",
body: {
...ifExistsBody,
encoding,
files: pathOrFiles
}
Expand All @@ -365,6 +377,8 @@ function createFileReadRequest(pathOrFiles, encoding) {
return {
method: "POST",
body: {
...ifExistsBody,
...(typeof pathOrFiles.ifExists === "boolean" ? { ifExists: pathOrFiles.ifExists } : {}),
encoding: pathOrFiles.encoding ?? encoding,
files: pathOrFiles.files
}
Expand All @@ -375,6 +389,8 @@ function createFileReadRequest(pathOrFiles, encoding) {
return {
method: "POST",
body: {
...ifExistsBody,
...(typeof pathOrFiles.ifExists === "boolean" ? { ifExists: pathOrFiles.ifExists } : {}),
encoding: pathOrFiles.encoding ?? encoding,
path: pathOrFiles.path
}
Expand All @@ -384,6 +400,7 @@ function createFileReadRequest(pathOrFiles, encoding) {
return {
method: "GET",
query: {
...ifExistsQuery,
encoding,
path: pathOrFiles
}
Expand Down Expand Up @@ -1000,11 +1017,33 @@ export function createApiClient(options = {}) {
* `~` or `~/...` shorthand for the current user's `L2/<username>/...` path.
* It also accepts composed batch input through a `files` array.
*
* Pass `{ ifExists: true }` (either on the input object for the
* `{path}` / `{files}` forms or as a third positional argument for the
* bare-path / array forms) to opt into idempotent semantics: paths that
* do not exist return `200`. The singular form returns
* `{ content: null, encoding: null, path: null, skipped: [requested] }`;
* the batch form returns the read files plus a `skipped[]` field for
* any missing entries. Without `ifExists` the call stays strict so
* callers that need authoritative 404 keep their existing behaviour.
*
* Idempotent reads bypass the file-read batching queue so the option
* is applied per-call and missing paths cannot poison a shared batch.
*
* @param {string | FileReadInput[] | FileReadBatchOptions | FileReadInput} pathOrFiles
* @param {string} [encoding]
* @param {{ ifExists?: boolean }} [options]
* @returns {Promise<FileApiResult | FileBatchApiResult>}
*/
async function fileRead(pathOrFiles, encoding = "utf8") {
async function fileRead(pathOrFiles, encoding = "utf8", options) {
const optionsObject = isPlainObject(options) ? options : {};
const inputIfExists =
isPlainObject(pathOrFiles) && typeof pathOrFiles.ifExists === "boolean" ? pathOrFiles.ifExists : null;
const ifExistsFlag = inputIfExists === true || optionsObject.ifExists === true;

if (ifExistsFlag) {
return call("file_read", createFileReadRequest(pathOrFiles, encoding, { ifExists: true }));
}

return queueFileRead(pathOrFiles, encoding);
}

Expand Down
10 changes: 4 additions & 6 deletions app/L0/_all/mod/_core/login_hooks/login-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,12 @@ async function hasFirstLoginMarker(runtime, markerPath) {
}
}

// fileRead-fallback when fileInfo is not available. Idempotent read so a
// missing marker resolves cleanly without console-spamming a 404.
try {
await runtime.api.fileRead(markerPath);
return true;
const result = await runtime.api.fileRead(markerPath, "utf8", { ifExists: true });
return typeof result?.content === "string";
} catch (error) {
if (isMissingFileError(error)) {
return false;
}

throw error;
}
}
Expand Down
107 changes: 58 additions & 49 deletions app/L0/_all/mod/_core/onscreen_agent/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,6 @@ function getRuntime() {
return runtime;
}

function isMissingFileError(error) {
const message = String(error?.message || "");
return /\bstatus 404\b/u.test(message) || /File not found\./u.test(message);
}

function isSingleUserAppRuntime(runtime) {
return Boolean(runtime?.config?.get?.("SINGLE_USER_APP", false));
}
Expand Down Expand Up @@ -388,52 +383,58 @@ export async function loadOnscreenAgentConfig() {
const runtime = getRuntime();
const uiStateOwner = await getUiStateOwner(runtime);

// Idempotent read: a fresh user has no `~/conf/onscreen-agent.yaml` yet.
// Use ifExists so the missing file returns content: null instead of
// throwing 404 on every space switch and console-spamming the user.
let result;
try {
const result = await runtime.api.fileRead(config.ONSCREEN_AGENT_CONFIG_PATH);
const normalizedConfig = await normalizeStoredConfig(
runtime,
runtime.utils.yaml.parse(String(result?.content || ""))
);
result = await runtime.api.fileRead(config.ONSCREEN_AGENT_CONFIG_PATH, "utf8", { ifExists: true });
} catch (error) {
throw new Error(`Unable to load onscreen agent config: ${error.message}`);
}

if (typeof result?.content !== "string") {
// Missing config: fall through to first-run defaults with optional UI state replay.
const storedUiState =
loadUiStateFromStorageArea("sessionStorage", { owner: uiStateOwner }) ||
loadUiStateFromStorageArea("localStorage", { owner: uiStateOwner }) ||
normalizeStoredUiState(normalizedConfig);
loadUiStateFromStorageArea("sessionStorage", { allowUnowned: false, owner: uiStateOwner }) ||
loadUiStateFromStorageArea("localStorage", { allowUnowned: false, owner: uiStateOwner });
const defaultConfig = createDefaultConfig();

return {
settings: normalizedConfig.settings,
systemPrompt: normalizedConfig.systemPrompt,
...storedUiState,
uiStateOwner,
shouldCenterInitialPosition: false
};
} catch (error) {
if (isMissingFileError(error)) {
const storedUiState =
loadUiStateFromStorageArea("sessionStorage", { allowUnowned: false, owner: uiStateOwner }) ||
loadUiStateFromStorageArea("localStorage", { allowUnowned: false, owner: uiStateOwner });
const defaultConfig = createDefaultConfig();

if (storedUiState) {
return {
settings: defaultConfig.settings,
systemPrompt: defaultConfig.systemPrompt,
...storedUiState,
uiStateOwner,
shouldCenterInitialPosition: false
};
}

// A missing per-user config with no owner-tagged UI state means first-run defaults for this load.
if (storedUiState) {
return {
...defaultConfig,
...createDefaultUiState(),
settings: defaultConfig.settings,
systemPrompt: defaultConfig.systemPrompt,
...storedUiState,
uiStateOwner,
shouldCenterInitialPosition: true
shouldCenterInitialPosition: false
};
}

throw new Error(`Unable to load onscreen agent config: ${error.message}`);
// A missing per-user config with no owner-tagged UI state means first-run defaults for this load.
return {
...defaultConfig,
...createDefaultUiState(),
uiStateOwner,
shouldCenterInitialPosition: true
};
}

const normalizedConfig = await normalizeStoredConfig(
runtime,
runtime.utils.yaml.parse(result.content)
);
const storedUiState =
loadUiStateFromStorageArea("sessionStorage", { owner: uiStateOwner }) ||
loadUiStateFromStorageArea("localStorage", { owner: uiStateOwner }) ||
normalizeStoredUiState(normalizedConfig);

return {
settings: normalizedConfig.settings,
systemPrompt: normalizedConfig.systemPrompt,
...storedUiState,
uiStateOwner,
shouldCenterInitialPosition: false
};
}

export async function saveOnscreenAgentConfig(nextConfig) {
Expand All @@ -460,20 +461,28 @@ export function saveOnscreenAgentUiState(nextState) {
export async function loadOnscreenAgentHistory() {
const runtime = getRuntime();

// Idempotent read: history file may not exist on first run. ifExists
// returns content: null instead of throwing 404.
let result;
try {
const result = await runtime.api.fileRead(config.ONSCREEN_AGENT_HISTORY_PATH);
const parsed = JSON.parse(String(result?.content || "[]"));
return Array.isArray(parsed) ? parsed : [];
result = await runtime.api.fileRead(config.ONSCREEN_AGENT_HISTORY_PATH, "utf8", { ifExists: true });
} catch (error) {
if (isMissingFileError(error)) {
return [];
}
throw new Error(`Unable to load onscreen agent history: ${error.message}`);
}

if (typeof result?.content !== "string") {
return [];
}

try {
const parsed = JSON.parse(result.content || "[]");
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("Unable to load onscreen agent history: invalid JSON.");
}

throw new Error(`Unable to load onscreen agent history: ${error.message}`);
throw error;
}
}

Expand Down
8 changes: 7 additions & 1 deletion app/L0/_all/mod/_core/panels/panel-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,14 @@ export async function listPanels() {
return [];
}

// Idempotent batch read: panel manifests can be removed by module_remove
// between the listing and this read. The panels parser keys files by
// path through a Map, so missing entries simply do not appear in the
// lookup — same shape we get from a 200 with `skipped`, just without
// the 404 console noise.
const result = await runtime.api.fileRead({
files: manifestFiles.map((manifestFile) => manifestFile.filePath)
files: manifestFiles.map((manifestFile) => manifestFile.filePath),
ifExists: true
});
const files = Array.isArray(result?.files) ? result.files : [];
const fileMap = new Map(
Expand Down
Loading