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.
Summary
mem::obsidian-export(REST:POST /agentmemory/obsidian/export, and the auto-export path inmem::consolidate-pipeline) throws an unhandledTypeErrorand returns HTTP 500 with body{"error":"[object Object]"}whenever any session (or memory/lesson/crystal) in the store is missing itsidfield. 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
main,src/functions/obsidian-export.ts@c9a0d29)id/startedAt(orphaned/partial session rows).Reproduction
With at least one session row that has
id === undefined: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 viasanitize(X.id)before the per-itemtry { … }:sanitize(name: string)doesname.replace(/.../g, "_"). Whens.idisundefined,undefined.replace(...)throwsTypeError: Cannot read properties of undefined (reading 'replace'). Because the throw is outside thetry, it escapes the wholeregisterFunctionhandler. The iii engine then serializes the thrownErrorobject 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, andcrystals. The session sortnew Date(b.startedAt).getTime()is also fragile whenstartedAtis missing (yieldsNaN), though that alone doesn't throw.Impact
OBSIDIAN_AUTO_EXPORT=truevia the consolidation pipeline).[object Object]), so it's hard to diagnose without reading source.Suggested fix
Skip id-less records and/or move
sanitize(id)inside thetry, and null-guard the sort. Minimal version: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/exportappears to parsetypesas 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-leveltypes: string[]contract — worth aligning.Happy to open a PR with the guard + a regression test (an id-less session fixture) if useful.