Commit Graph

16 Commits

Author SHA1 Message Date
Hoshina ca9652e120 refactor(session): replace dm scope with dimensions policy 2026-04-01 17:19:50 +08:00
Hoshina 3957e2cc72 feat(session): persist scope metadata and aliases 2026-04-01 16:25:05 +08:00
wenjie e74820cf69 fix: skip meta json files during session migration (#1340) 2026-03-11 14:29:42 +08:00
statxc 2e3e6788ab fix(session): sanitize '/' and '\' in session keys so forum topic keys don't create invalid paths 2026-03-10 16:11:34 +00:00
xiaoen f9f726c0c1 fix(memory): fsync appended message for consistent durability
addMsg now calls f.Sync() before f.Close(), matching the durability
guarantee of writeMeta and rewriteJSONL (both use WriteFileAtomic
with fsync). Without this, a power loss could leave the appended
line in the kernel page cache only — lost on reboot.
2026-03-04 19:21:34 +08:00
xiaoen 6d894d6138 refactor(memory): use fileutil.WriteFileAtomic and log corrupt lines
- Replace manual temp+rename in writeMeta and rewriteJSONL with the
  project's standard fileutil.WriteFileAtomic. This adds fsync before
  rename, which is important for flash storage on embedded devices
  where power loss can leave zero-length files after an unsynced rename.
- Log a warning when readMessages skips a corrupt line, so operators
  can see that data was lost after a crash instead of silently dropping it.
- Document the lossy sanitizeKey mapping (telegram:123 → telegram_123)
  as an intentional tradeoff.
2026-03-01 14:46:54 +08:00
xiaoen e810331dd8 fix(memory): use SetHistory in migration for crash idempotency
MigrateFromJSON previously called AddFullMessage in a loop, then
renamed the .json file to .json.migrated. If the process crashed
after appending some messages but before the rename, a retry would
re-read the same .json and append all messages again — duplicating
whatever was written before the crash.

Switch to SetHistory which atomically replaces the session contents.
A retry after crash overwrites the partial data instead of appending.
2026-02-26 16:15:11 +08:00
xiaoen 9c72317b9b fix(memory): write meta before JSONL rewrite for crash safety
In SetHistory and Compact, the JSONL file was rewritten before updating
the meta file. If the process crashed between the two writes, the meta
still had a large Skip value pointing past the now-shorter JSONL file,
causing GetHistory to return empty — effectively data loss.

Reverse the order: write meta (with Skip=0) first, then rewrite JSONL.
On crash between the two writes, the old uncompacted file is still
intact and GetHistory reads from line 1, returning stale-but-complete
data. The next operation self-corrects.
2026-02-26 16:13:57 +08:00
xiaoen 1f0b85280a fix(memory): always reconcile line count in TruncateHistory
A crash between the JSONL append and the meta update in addMsg can
leave meta.Count stale (e.g. file has 101 lines but meta says 100).
The previous code only reconciled when Count==0, so a nonzero stale
count was silently trusted, causing keepLast/skip to be calculated
against the wrong total.

Now TruncateHistory always counts the actual lines on disk. This is
cheap (scan without unmarshal) and TruncateHistory is not a hot path.
2026-02-26 16:12:34 +08:00
xiaoen d55e5540af fix(memory): bound lock memory and increase scanner buffer
Address feedback from @yinwm for long-running daemon use:

- Replace sync.Map with a fixed-size sharded lock array (64 mutexes).
  Keys are mapped via FNV hash, so memory is O(1) regardless of how
  many sessions are created over the process lifetime.

- Increase scanner buffer cap from 1 MB to 10 MB. Tool results
  (read_file on large files, web search responses) can easily exceed
  1 MB. The scanner still starts at 64 KB and only grows as needed.
2026-02-26 15:35:04 +08:00
xiaoen 5d73ee2d9a refactor(memory): use sync.Map for session locks and skip-scan in readMessages
Address review feedback from @Zhaoyikaiii:

- Replace map[string]*sync.Mutex + separate mu with sync.Map.LoadOrStore
  for simpler, lock-free session lock management.

- Add skip parameter to readMessages so callers (GetHistory, Compact)
  can skip truncated lines without paying the json.Unmarshal cost.

- Add countLines helper for TruncateHistory's count reconciliation,
  avoiding full deserialization when only the line count is needed.
2026-02-26 14:31:02 +08:00
xiaoen b464687e2f feat(memory): add Compact method for physical JSONL compaction
Address file growth concern from #711 review: logical truncation via
skip offset is fast but leaves dead lines on disk indefinitely.

Compact() rewrites the JSONL file keeping only active messages, using
the same temp+rename pattern for crash safety. No-op when skip == 0.
The caller (lifecycle manager or agent loop) decides when to trigger
compaction — e.g. when skipped lines exceed active lines.
2026-02-26 08:42:35 +08:00
xiaoen 903681207b feat(memory): support migration from legacy JSON sessions
Read existing sessions/*.json files, convert to JSONL format, and
rename originals to .json.migrated as backup. The migration is
idempotent — second runs skip already-migrated files.

Session keys are read from JSON content (not filenames) so that
sanitized names like telegram_123 correctly map back to telegram:123.
2026-02-26 08:35:05 +08:00
xiaoen 529622b7d3 test(memory): add unit, concurrency, and benchmark tests
Cover all Store interface methods plus edge cases:
- Basic roundtrip, ordering, empty session, tool calls
- Logical truncation (keep last N, keep zero, keep more than exist)
- SetHistory replacing all + resetting skip offset
- Crash recovery with partial JSON lines
- Persistence across store instances
- Concurrent add+read (10 goroutines x 20 msgs)
- Simulated #704 race (summarizer vs main loop)
- Benchmarks for AddMessage and GetHistory (100/1000 msgs)
2026-02-26 08:35:04 +08:00
xiaoen 9f36e50807 feat(memory): implement append-only JSONL session store
Add JSONLStore that persists sessions as .jsonl files (one message per
line) plus .meta.json for summary and truncation offset.

Key design decisions:
- Append-only writes — no full-file rewrites on AddMessage
- Logical truncation via skip offset instead of physical deletion
- Per-session mutex for safe concurrent access
- Crash recovery: malformed trailing lines are silently skipped
- Atomic metadata writes using temp+rename

Zero new dependencies — pure stdlib.

Refs #711
2026-02-26 08:35:04 +08:00
xiaoen 32ec8cadeb feat(memory): define Store interface for session persistence
Introduce a backend-agnostic Store interface in pkg/memory/ that maps
one-to-one with the current SessionManager API. Each method is atomic
— no separate Save() call needed.

Refs #711
2026-02-26 08:35:04 +08:00