Skip to content

mem::obsidian-export crashes with 500 "[object Object]" when any session/record lacks an id (unguarded sanitize() outside try/catch) #729

Description

@smatty-ice

Summary

mem::obsidian-export (REST: POST /agentmemory/obsidian/export, and the auto-export path in mem::consolidate-pipeline) throws an unhandled TypeError and returns HTTP 500 with body {"error":"[object Object]"} whenever any session (or memory/lesson/crystal) in the store is missing its id field. A single malformed/orphaned record poisons the entire export — zero files are written.

This is the export-side analogue of #365 (which fixed the same "session missing id" crash class in the viewer/dashboard). The export function was not hardened the same way.

Environment

  • agentmemory v0.9.24 (also present in current main, src/functions/obsidian-export.ts @ c9a0d29)
  • macOS (Apple Silicon), Node 20, iii-engine v0.11.2
  • Reproduced with a real store where 7 of 13 sessions had no id/startedAt (orphaned/partial session rows).

Reproduction

With at least one session row that has id === undefined:

curl -s -X POST http://localhost:3111/agentmemory/obsidian/export \
  -H "Content-Type: application/json" \
  -d '{"vaultDir":"/Users/me/.agentmemory/vault","types":["sessions"]}'
# → {"error":"[object Object]","error_id":"..."}  (HTTP 500), 0 files written

types: ["sessions"] is enough — no LLM/memories required, so it's clearly the file-writing loop, not consolidation.

Root cause

In src/functions/obsidian-export.ts, every export loop builds the filename via sanitize(X.id) before the per-item try { … }:

for (const s of recent) {
  const filename = `${sanitize(s.id)}.md`;   // ⬅️ outside try — throws here
  const filepath = join(dirs.sessions, filename);
  try {
    await writeFile(filepath, sessionToMd(s));
    ...
  } catch (err) { errors.push(...) }          // never catches the line above
}

sanitize(name: string) does name.replace(/.../g, "_"). When s.id is undefined, undefined.replace(...) throws TypeError: Cannot read properties of undefined (reading 'replace'). Because the throw is outside the try, it escapes the whole registerFunction handler. The iii engine then serializes the thrown Error object into the HTTP response without stringifying it, producing the opaque {"error":"[object Object]"} (a separate, secondary papercut worth fixing in the error-serialization layer).

The same unguarded pattern exists for memories (sanitize(m.id)), lessons, and crystals. The session sort new Date(b.startedAt).getTime() is also fragile when startedAt is missing (yields NaN), though that alone doesn't throw.

Impact

  • One malformed record makes all Obsidian export fail (manual route and OBSIDIAN_AUTO_EXPORT=true via the consolidation pipeline).
  • Failure is opaque ([object Object]), so it's hard to diagnose without reading source.

Suggested fix

Skip id-less records and/or move sanitize(id) inside the try, and null-guard the sort. Minimal version:

for (const s of sessions
  .filter((s) => s && s.id)                                   // drop id-less rows
  .sort((a, b) => new Date(b.startedAt || 0).getTime()
                - new Date(a.startedAt || 0).getTime())
  .slice(0, 50)) {
  try {
    const filename = `${sanitize(String(s.id))}.md`;          // inside try
    await writeFile(join(dirs.sessions, filename), sessionToMd(s));
    ...
  } catch (err) { errors.push(...) }
}

Apply the same filter(x => x && x.id) + sanitize(String(x.id)) guard to the memories/lessons/crystals loops. Optionally log how many records were skipped for visibility.

Secondarily, consider stringifying thrown errors in the REST/function error path so failures surface as a real message instead of [object Object].

Minor, possibly related

The REST wrapper for /agentmemory/obsidian/export appears to parse types as a comma-separated string ("sessions"), so a JSON array ["sessions"] is silently ignored and all types are exported. Not the cause of this bug, but inconsistent with the function-level types: string[] contract — worth aligning.

Happy to open a PR with the guard + a regression test (an id-less session fixture) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions