{"record":{"id":"4e01d6c7e4402044","repo":"NousResearch/hermes-agent","slug":"saving-is-not-available","errorCode":null,"errorMessage":"Saving is not available","messagePattern":"Saving is not available","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/desktop/src/lib/desktop-fs.ts","lineNumber":91,"sourceCode":"\nexport async function readDesktopFileText(path: string): Promise<HermesReadFileTextResult> {\n  if (!isDesktopFsRemoteMode()) {\n    return bridge().readFileText(path)\n  }\n\n  return remoteFsApi<HermesReadFileTextResult>(fsPath('read-text', path))\n}\n\n// Save UTF-8 text back to a file. Local writes go through the hardened Electron\n// IPC; remote writes hit the dashboard's POST /api/fs/write-text (same path\n// hardening, parent-must-exist, size cap) so the editor behaves identically in\n// both modes. Stale-on-disk detection is the caller's job (re-read before save).\nexport async function writeDesktopFileText(path: string, content: string): Promise<{ path: string }> {\n  const desktop = bridge()\n\n  if (!isDesktopFsRemoteMode()) {\n    if (!desktop.writeTextFile) {\n      throw new Error('Saving is not available')\n    }\n\n    return desktop.writeTextFile(path, content)\n  }\n\n  const result = await remoteFsApi<{ ok?: boolean; path?: string }>('/api/fs/write-text', { content, path })\n\n  return { path: result.path || path }\n}\n\nexport async function readDesktopFileDataUrl(path: string): Promise<string> {\n  if (!isDesktopFsRemoteMode()) {\n    return bridge().readFileDataUrl(path)\n  }\n\n  const result = await remoteFsApi<string | { dataUrl?: string }>(fsPath('read-data-url', path))\n\n  return typeof result === 'string' ? result : result.dataUrl || ''","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/src/lib/desktop-fs.ts#L73-L109","documentation":"Thrown by writeDesktopFileText() in apps/desktop/src/lib/desktop-fs.ts:91 when running in LOCAL mode (not remote FS mode) and the bridge object lacks a `writeTextFile` method. The hardened Electron IPC save path only exists in shells that expose it; an older desktop build, a stripped preload, or a non-desktop environment with a partial hermesDesktop stub reaches this guard and saving is refused rather than silently doing nothing.","triggerScenarios":"Calling writeDesktopFileText in local mode on an older Electron shell that predates the writeTextFile IPC; a test/mocked bridge exposing only `api`; a preview environment where the bridge exists but file-write capabilities were not injected.","commonSituations":"Desktop app and runtime on separate clocks after an update (renderer newer than shell); running the renderer in a browser with a partial hermesDesktop shim; sandboxed contexts where the write IPC was deliberately omitted.","solutions":["Update the desktop shell so the preload exposes writeTextFile (restart the app after update — the hook's own copy recommends a restart for similar IPC gaps).","Guard the save UI: disable the Save action when `!window.hermesDesktop?.writeTextFile && !isDesktopFsRemoteMode()`.","In remote mode this path isn't taken — connect to the remote gateway so writes go through POST /api/fs/write-text.","For tests, stub writeTextFile on the fake bridge."],"exampleFix":"// before\nawait writeDesktopFileText(path, content) // throws on old shell\n\n// after\nconst canSave = isDesktopFsRemoteMode() || Boolean(window.hermesDesktop?.writeTextFile)\nif (!canSave) { setSaveUnavailable('Update the desktop app to enable saving'); return }\nawait writeDesktopFileText(path, content)","handlingStrategy":"type-guard","validationCode":"function canWriteFiles(): boolean {\n  return isDesktopFsRemoteMode() || typeof window.hermesDesktop?.writeTextFile === 'function'\n}","typeGuard":"function hasWriteTextFile(w: Window): w is Window & { hermesDesktop: { writeTextFile: (p: string, c: string) => Promise<unknown> } } {\n  return typeof (w as any).hermesDesktop?.writeTextFile === 'function'\n}","tryCatchPattern":"if (!canWriteFiles()) { setSaveDisabled('Update the desktop app to enable saving'); return }\ntry { await writeDesktopFileText(path, content) } catch (e) { notifyError(e, 'Save failed') }","preventionTips":["Bind the Save button's enabled state to the capability, not to view state","Keep shell and renderer versions in lockstep (update + restart together)","Remote mode routes writes through POST /api/fs/write-text — prefer it when bridging a remote backend","Stub writeTextFile in bridge mocks"],"tags":["desktop","electron","filesystem","version-skew","save"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}