Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions specstory-cli/pkg/session/filename_unique_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package session

import (
"strings"
"testing"

"github.com/specstoryai/getspecstory/specstory-cli/pkg/spi"
)

// A /branch fork shares the parent's start time + first message (→ same slug),
// so without the session-id suffix the two would collide on one filename.
func TestBuildSessionFilePath_BranchDoesNotCollide(t *testing.T) {
parent := &spi.AgentChatSession{
SessionID: "26791918-7e65-4217-878d-c33751b1cce9",
CreatedAt: "2026-06-19T05:04:33.976Z",
Slug: "so-you-can-read",
}
branch := &spi.AgentChatSession{
SessionID: "2e951ffb-1111-2222-3333-444455556666",
CreatedAt: "2026-06-19T05:04:33.976Z",
Slug: "so-you-can-read",
}

p := BuildSessionFilePath(parent, "/h", true)
b := BuildSessionFilePath(branch, "/h", true)

if p == b {
t.Fatalf("branch collided with parent: both -> %s", p)
}
if !strings.HasSuffix(p, "-26791918.md") {
t.Errorf("parent path missing session-id suffix: %s", p)
}
if !strings.HasSuffix(b, "-2e951ffb.md") {
t.Errorf("branch path missing session-id suffix: %s", b)
}
}
16 changes: 16 additions & 0 deletions specstory-cli/pkg/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,25 @@ func BuildSessionFilePath(session *spi.AgentChatSession, historyDir string, useU
if session.Slug != "" {
filename = fmt.Sprintf("%s-%s", timestampStr, session.Slug)
}
// Append a short session-id suffix so every session gets a unique filename.
// Without it, sessions sharing a start-second + first message — e.g. a /branch
// fork, which inherits the parent's opening verbatim — collide on the same name
// and overwrite each other's .md.
if sid := shortSessionID(session.SessionID); sid != "" {
filename = fmt.Sprintf("%s-%s", filename, sid)
}
return filepath.Join(historyDir, filename+".md")
}

// shortSessionID returns the leading 8 chars of a session id — enough to
// disambiguate sessions that would otherwise share a filename.
func shortSessionID(id string) string {
if len(id) > 8 {
return id[:8]
}
return id
}

// ProcessingOptions holds flags that control session processing behavior.
type ProcessingOptions struct {
OnlyCloudSync bool // Skip local markdown writes and only sync to cloud
Expand Down