Skip to content
Draft
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ Individual voice or chat conversations. Captures transcript, AI summary, tool ca
syllable sessions list [--page N] [--limit N] [--start-date DATE] [--end-date DATE]
syllable sessions get <session-id>
syllable sessions transcript <session-id>
syllable sessions timeline <session-id>
syllable sessions summary <session-id>
syllable sessions latency <session-id>
syllable sessions recording <session-id>
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,13 @@ syllable tools history <id>

### Sessions

Sessions are individual voice or chat conversations. Each session captures the transcript, an AI summary, all tool calls with their arguments and API responses, duration (for voice), channel, and test/live status. Use `transcript`, `summary`, `latency`, and `recording` subcommands to pull specific session data.
Sessions are individual voice or chat conversations. Each session captures the transcript, an AI summary, all tool calls with their arguments and API responses, duration (for voice), channel, and test/live status. Use `transcript`, `timeline`, `summary`, `latency`, and `recording` subcommands to pull specific session data. The `timeline` subcommand returns a single consolidated, time-ordered stream of transcript turns, tool calls, and latency events with offsets from the session start.

```bash
syllable sessions list [--start-date DATE] [--end-date DATE]
syllable sessions get <id>
syllable sessions transcript <id>
syllable sessions timeline <id>
syllable sessions summary <id>
syllable sessions latency <id>
syllable sessions recording <id>
Expand Down
59 changes: 59 additions & 0 deletions scripts/syllable-cli/cmd/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func sessionsCmd() *cobra.Command {
cmd.AddCommand(sessionsListCmd())
cmd.AddCommand(sessionsGetCmd())
cmd.AddCommand(sessionsTranscriptCmd())
cmd.AddCommand(sessionsTimelineCmd())
cmd.AddCommand(sessionsSummaryCmd())
cmd.AddCommand(sessionsLatencyCmd())
cmd.AddCommand(sessionsRecordingCmd())
Expand Down Expand Up @@ -283,6 +284,64 @@ func sessionsTranscriptCmd() *cobra.Command {
}
}

func sessionsTimelineCmd() *cobra.Command {
return &cobra.Command{
Use: "timeline <session-id>",
Short: "Get the consolidated, time-ordered timeline for a session",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
data, _, err := apiClient.Get("/api/v1/sessions/timeline/" + url.PathEscape(args[0]))
if err != nil {
return err
}

if getOutputFmt() == "json" {
output.PrintJSON(data)
return nil
}

var result struct {
SessionID string `json:"session_id"`
Events []struct {
Kind string `json:"kind"`
Offset string `json:"offset"`
Source *string `json:"source"`
Text *string `json:"text"`
Label *string `json:"label"`
DurationMs *float64 `json:"duration_ms"`
} `json:"events"`
}
if err := json.Unmarshal(data, &result); err != nil {
output.PrintJSON(data)
return nil
}

fmt.Printf("Session: %s\n\n", result.SessionID)
headers := []string{"OFFSET", "KIND", "SOURCE", "LABEL", "DETAIL"}
rows := make([][]string, len(result.Events))
for i, e := range result.Events {
src := ""
if e.Source != nil {
src = *e.Source
}
lbl := ""
if e.Label != nil {
lbl = *e.Label
}
detail := ""
if e.Text != nil && *e.Text != "" {
detail = output.Truncate(*e.Text, 60)
} else if e.DurationMs != nil {
detail = fmt.Sprintf("%.0f ms", *e.DurationMs)
}
rows[i] = []string{e.Offset, e.Kind, src, lbl, detail}
}
printTable(headers, rows)
return nil
},
}
}

func sessionsSummaryCmd() *cobra.Command {
return &cobra.Command{
Use: "summary <session-id>",
Expand Down
30 changes: 30 additions & 0 deletions scripts/syllable-cli/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package integration_test

import (
"encoding/json"
"fmt"
"os"
"os/signal"
Expand Down Expand Up @@ -471,6 +472,35 @@ func TestAgentsLabels(t *testing.T) {
mustRunCLI(t, "agents", "labels")
}

// ---------------------------------------------------------------------------
// Session Timeline (read-only) — spec-sync
// ---------------------------------------------------------------------------

func TestSessionsTimeline(t *testing.T) {
// List sessions, then fetch the timeline for the first one. A fresh CI org
// may have no sessions, so skip rather than fail when the list is empty.
// --include-test so a throwaway CI org (whose sessions are likely test
// sessions) still has a session to exercise the timeline endpoint against.
out := mustRunCLI(t, "sessions", "list", "--limit", "1", "--include-test")
var listed struct {
Items []struct {
SessionID string `json:"session_id"`
} `json:"items"`
}
if err := json.Unmarshal(out, &listed); err != nil {
t.Fatalf("parse sessions list: %v (raw: %s)", err, string(out))
}
if len(listed.Items) == 0 || listed.Items[0].SessionID == "" {
t.Skip("no sessions in this org; skipping timeline test")
}

// A clean exit proves the path, auth, and response parsing all work.
timeline := mustRunCLI(t, "sessions", "timeline", listed.Items[0].SessionID)
if !json.Valid(timeline) {
t.Fatalf("timeline output is not valid JSON: %s", string(timeline))
}
}

// ---------------------------------------------------------------------------
// Organization SIP IP Ranges — spec-sync v0.0.3
// ---------------------------------------------------------------------------
Expand Down
Loading