{"record":{"id":"fa1b061a357bb432","repo":"multica-ai/multica","slug":"read-hermes-memory-store-s-w","errorCode":null,"errorMessage":"read hermes memory store %s: %w","messagePattern":"read hermes memory store (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/internal/daemon/execenv/hermes_memory.go","lineNumber":217,"sourceCode":"//     error reading either directory, or an entry it will not copy (symlink,\n//     device node — a link copied verbatim could point the store outside\n//     itself), is an error, not a silent skip.\n//   - It publishes with a single atomic rename from a staging directory, so\n//     two tasks of the same agent migrating at once cannot interleave into one\n//     half-populated store. First writer wins; the loser discards its staging\n//     and takes the winner's store, which is the same outcome as finding a\n//     store that was already populated.\n//\n// Only an absent or empty store is migrated into, so an upgrade never\n// overwrites memory the agent has already accumulated.\nfunc migrateHermesTaskMemories(taskDir, storeDir string, logger *slog.Logger) error {\n\tstored, err := os.ReadDir(storeDir)\n\tswitch {\n\tcase err == nil && len(stored) > 0:\n\t\treturn nil // store already holds this agent's memory — never overwrite it\n\tcase err != nil && !os.IsNotExist(err):\n\t\t// Treating this as \"nothing to migrate\" would delete the source below.\n\t\treturn fmt.Errorf(\"read hermes memory store %s: %w\", storeDir, err)\n\t}\n\n\tentries, err := os.ReadDir(taskDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read task-local hermes memories %s: %w\", taskDir, err)\n\t}\n\tif len(entries) == 0 {\n\t\treturn nil\n\t}\n\n\tstaging, err := newHermesStoreStaging(storeDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(staging) // no-op once the staging dir has been promoted\n\n\tfor _, entry := range entries {\n\t\tsrc := filepath.Join(taskDir, entry.Name())","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemon/execenv/hermes_memory.go#L199-L235","documentation":"Returned by migrateHermesTaskMemories when os.ReadDir on the destination store fails with anything other than not-exist. The code must distinguish 'empty store, safe to migrate into' from 'unreadable store'; the comment notes that treating an unread error as empty would let the caller later delete the source memories dir that was never copied — so it fails closed.","triggerScenarios":"os.ReadDir(storeDir) returns EACCES/EPERM (store dir exists with 0700 owned by another user) or EIO; any non-ENOENT errno on the store directory read.","commonSituations":"Daemon runs under a different user than the one that created the store during an earlier upgrade; a partially-restored backup where the store dir exists but has broken mode bits.","solutions":["chown/chmod the store dir (needs 0700, daemon-readable) to the daemon user: chmod 700 <profile>/hermes-state/<agent>/<segment>.","If the store is definitively empty/unwanted, remove it so ReadDir gets ENOENT and migration proceeds cleanly.","Verify only one daemon user writes to the profile dir."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before triggering migration, confirm the store is readable:\nif ents, err := os.ReadDir(storeDir); err != nil && !os.IsNotExist(err) {\n    return fmt.Errorf(\"store unreadable — fix %s before upgrade: %w\", storeDir, err)\n} else if err == nil && len(ents) > 0 {\n    // populated: migration will be skipped, nothing to do\n}","typeGuard":null,"tryCatchPattern":"if err := migrateHermesTaskMemories(src, store, logger); err != nil {\n    if strings.Contains(err.Error(), \"read hermes memory store\") {\n        // fail-closed by design: NEVER swallow and delete the source; fix perms and retry\n    }\n}","preventionTips":["Never change the OS user that runs the daemon mid-upgrade","Keep store dirs 0700 daemon-owned","Do not treat this error as 'nothing to migrate' — the source would be deleted"],"tags":["filesystem","hermes","migration","permissions","fail-closed"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}