v0.10.3 — parallel in-app conversations + cron-leak fix
0.10.3 — 2026-05-11
One feature, one fix. Both surfaced by real usage.
Parallel in-app conversations per agent
Hold N independent chats with the same agent in the Obsidian chat panel — different tasks, different contexts, switchable from a dropdown. Reuses the conversation plumbing that already powers Slack multi-thread.
- Same-agent dedup is now
(agent, conversationId)dedup. Two tabs on the same agent are fine; two tabs on the same(agent, conversation)reveals the existing tab. - A conversation picker row appears below the agent dropdown whenever the agent has more than one conversation. Single-conversation users see no UI change.
- + New Chat creates a new conversation alongside the current one (was: wipe-the-singleton). Rename / Delete buttons next to the picker; the legacy "Main chat" is delete-protected so pre-feature data can't be orphaned accidentally.
- Tab title becomes
Chat: <agent> · <conversation name>once a second conversation exists.
Data layout mirrors Slack sessions:
- Folder agents:
<agent-folder>/conversations/<convId>.json - Flat agents:
_fleet/memory/<agent>-conversations/<convId>.json - Sentinel id
"default"maps back to the legacy per-agentchat.jsonso existing data is untouched. The picker surfaces it as "Main chat".
State persistence: AgentChatView.getState() now returns {agentName, conversationId}. Reload restores the exact same tab → same conversation. Older state files without conversationId fall back to "default".
Engine unchanged: ChatSessionOptions.inAppConversationId joins existing channelName / conversationId / threadAnchorId knobs; getChatFilePath() grows one parallel branch. The Claude --resume flow, sendMessage lock, watchdog, threading, and stats all carry through.
Fix: stop old runtime's crons on saveSettings rebuild
saveSettings() was replacing this.runtime with a fresh FleetRuntime without stopping the outgoing runtime's heartbeat / task-scheduler crons. Croner timers own their own callbacks once started — dropping the reference doesn't stop them, so each leaked runtime kept firing the schedules it was registered with for the rest of the Obsidian session.
Concrete symptom: a user changes an agent's heartbeat from "every 2h" to "every 6h", and the 2h schedule keeps firing. Even though the agent-edit path correctly re-registered on the current runtime, any prior saveSettings() (toggling status bar, editing channel credentials, etc.) had leaked a previous runtime still holding a 2h cron. Multiple leaked runtimes accumulated across a session, causing duplicate runs and making schedule edits appear not to take effect until full reload.
TaskScheduler.shutdown()— stop every job, clear the map, drop the pending queue.FleetRuntime.shutdown()— stop heartbeat crons + delegate to scheduler. Idempotent.main.saveSettings()callsruntime.shutdown()before reassigning.main.onunload()callsruntime.shutdown()so "Disable plugin" without a full reload also tears down cleanly.
Regression test added on TaskScheduler — verifies post-shutdown resumeAll doesn't flush queued runs and no cron remains to fire.
Tests
108/108 green (up from 103): 4 new conversation-path tests on ChatSession + 1 scheduler-shutdown regression.
See v0.10.0 for the major Wiki Keeper / permissions release this patches.