fix(core): strip workspace prefix from cron/heartbeat session keys (#474)

In multi-workspace mode, stored session keys may be prefixed with the
workspace path (e.g. "/home/user/project:slack:C123:U456"). The naive
first-colon split produced a path as the platform name, causing
ExecuteCronJob and ExecuteHeartbeat to fail with "platform not found".

Add a fallback loop that searches for a registered platform name within
the key and strips the prefix before proceeding.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leigh StillardandClaude Sonnet 4.6 authored and GitHub committed 2026-04-08 22:26:31 +08:00
1 parent 6dd2155f87
commit 326fa258ff
2 files changed
+75

No files matched your search

+28
View File
@@ -814,6 +814,20 @@ func (e *Engine) ExecuteCronJob(job *CronJob) error {
break
}
}
// Fallback: in multi-workspace mode the stored session key may be prefixed
// with the workspace path (e.g. "/home/user/project:slack:C123:U456").
// Search for a known platform name within the key and strip the prefix.
if targetPlatform == nil {
for _, p := range e.platforms {
needle := ":" + p.Name() + ":"
if idx := strings.Index(sessionKey, needle); idx >= 0 {
targetPlatform = p
platformName = p.Name()
sessionKey = sessionKey[idx+1:] // strip workspace prefix
break
}
}
}
if targetPlatform == nil {
return fmt.Errorf("platform %q not found for session %q", platformName, sessionKey)
}
@@ -998,6 +1012,20 @@ func (e *Engine) ExecuteHeartbeat(sessionKey, prompt string, silent bool) error
break
}
}
// Fallback: in multi-workspace mode the stored session key may be prefixed
// with the workspace path (e.g. "/home/user/project:slack:C123:U456").
// Search for a known platform name within the key and strip the prefix.
if targetPlatform == nil {
for _, p := range e.platforms {
needle := ":" + p.Name() + ":"
if idx := strings.Index(sessionKey, needle); idx >= 0 {
targetPlatform = p
platformName = p.Name()
sessionKey = sessionKey[idx+1:] // strip workspace prefix
break
}
}
}
if targetPlatform == nil {
return fmt.Errorf("platform %q not found for session %q", platformName, sessionKey)
}
+47
View File
@@ -8107,6 +8107,53 @@ func TestExecuteCronJob_ResolvesCronReplyTarget(t *testing.T) {
}
}
func TestExecuteCronJob_WorkspacePrefixedSessionKey(t *testing.T) {
dir := t.TempDir()
store, err := NewCronStore(dir)
if err != nil {
t.Fatalf("NewCronStore() error = %v", err)
}
scheduler := NewCronScheduler(store)
platform := &stubCronReplyTargetPlatform{
stubPlatformEngine: stubPlatformEngine{n: "slack"},
}
agentSession := newResultAgentSession("done")
agent := &resultAgent{session: agentSession}
e := NewEngine("test", agent, []Platform{platform}, "", LangEnglish)
defer e.cancel()
e.cronScheduler = scheduler
// Simulate a session key that was stored with a workspace prefix
// (as happens in multi-workspace mode).
prefixedKey := "/home/user/workspace/myproject:slack:C123:U456"
job := &CronJob{
ID: "job-ws",
SessionKey: prefixedKey,
Prompt: "daily standup",
Description: "Standup",
}
if err := store.Add(job); err != nil {
t.Fatalf("store.Add() error = %v", err)
}
if err := e.ExecuteCronJob(job); err != nil {
t.Fatalf("ExecuteCronJob() with workspace-prefixed key error = %v", err)
}
// The platform should have received the cron start notice and agent reply.
sent := platform.getSent()
if len(sent) < 1 {
t.Fatalf("expected at least one message sent to platform, got %d", len(sent))
}
// Stored session key must remain unchanged.
if job.SessionKey != prefixedKey {
t.Fatalf("job.SessionKey = %q, want unchanged %q", job.SessionKey, prefixedKey)
}
}
func TestExtractSessionKeyParts(t *testing.T) {
tests := []struct {
name string