extractWorkspaceChannelKey rebuilt a workspace binding key from the
sessionKey alone. extractChannelID uses SplitN(key, ":", 4), so a Feishu
thread key "feishu:<chatID>:root:<rootID>" yielded just "<chatID>" and
every thread in a chat collapsed onto one binding. /status then reported
".", /list came back empty, /dir showed another conversation's directory,
and /new resolved an interactive key without the workspace prefix, so
cleanupInteractiveState missed the real state and the agent process leaked.
Platforms own their key layout, and core must not hardcode private markers
such as "root:", so this adds an optional ChannelScopedSessionKey interface
and asks the originating platform first. Feishu implements it by reusing
parseThreadRootID, matching what populateWorkspaceChannelKeys already
derives. Platforms without per-thread scoping implement nothing and keep
the previous behaviour, so this is purely additive: no interface signature
changes, and no other platform needs a line edited.
Two follow-on fixes the same key mismatch exposed:
- sessionContextForKey read the binding directly, but multi-workspace /dir
records a per-interactive-key override rather than rebinding the channel,
and only workspaceContext applies it. Resolving through workspaceContext
keeps /status and /dir consistent with the directory the agent uses.
- The SessionManager is per-workspace (getOrCreateWorkspaceAgent), so topics
sharing a directory shared every session in it. /list offered other
topics' sessions and /switch could resume one. applySessionFilter now
drops sessions owned by another sessionKey while keeping unowned ones,
which are CLI-created sessions that have no conversation to belong to.
applySessionFilter is inert outside multi-workspace mode, and the tests
cover both that and the unclaimed-key fallbacks.
Refs #193, which adds a separate /path command and leaves /dir global.