Skip to content
Merged
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
7 changes: 4 additions & 3 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,10 @@ sequences and displays at most five rendered output rows, retaining head and
tail context. The web client displays a five-line preview and provides a control
to expand retained results that exceed five lines.

Selecting a Session in the web chat opens it at the latest retained message.
Reconnecting preserves an intentional upward scroll position and shows the
history that remains available on the Session workspace.
Selecting a Session in the web chat opens a bounded page at the latest retained
message. Use **Load earlier messages** to prepend the previous page without
moving away from the current scroll position. Reconnecting preserves an
intentional upward scroll position and the loaded conversation view.

The Session sidebar shows compact relative activity times, and the selected
Session header shows whether it is active now, when it was last active, or when
Expand Down
6 changes: 4 additions & 2 deletions internal/manifests/charts/kelos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,10 @@ the underlying PersistentVolume.

The shared Session server serves the web application, bridges each chat to its
Session Pod through Kubernetes exec, and can request a destructive Session
workspace reset after user confirmation. It is disabled by default and requires
a non-empty static token in an existing Secret:
workspace reset after user confirmation. The web application opens long
conversations at a bounded recent page and loads earlier messages on demand. It
is disabled by default and requires a non-empty static token in an existing
Secret:

```bash
kubectl create secret generic kelos-session-auth \
Expand Down
5 changes: 4 additions & 1 deletion internal/sessionruntime/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func projectHistory(source []Event) ([]historyItem, HistoryState, []Event) {

turns := make(map[string]*historyTurn)
pendingInputs := make(map[string]Event)
fileDiff := ""
for index := range events {
event := events[index]
if event.TurnID != "" {
Expand Down Expand Up @@ -98,10 +99,12 @@ func projectHistory(source []Event) ([]historyItem, HistoryState, []Event) {
}
case EventInputResolved:
delete(pendingInputs, event.InputID)
case EventFileDiff:
fileDiff = boundedHistoryText(event.Diff, maxHistoryDiffBytes)
}
}

state := HistoryState{}
state := HistoryState{FileDiff: fileDiff}
queued := make([]HistoryQueuedTurn, 0)
queuedEventIDs := make(map[string]int64)
var activeEventID int64
Expand Down
1 change: 1 addition & 0 deletions internal/sessionruntime/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type HistoryState struct {
TurnInterrupting bool `json:"turnInterrupting,omitempty"`
WaitingForInput bool `json:"waitingForInput,omitempty"`
QueuedTurns []HistoryQueuedTurn `json:"queuedTurns,omitempty"`
FileDiff string `json:"fileDiff,omitempty"`
}

// HistoryQueuedTurn describes one user message waiting to run.
Expand Down
4 changes: 4 additions & 0 deletions internal/sessionruntime/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,7 @@ func TestServerPreservesStateOutsideProjectedHistoryPage(t *testing.T) {
startedAt := time.Date(2026, time.August, 8, 12, 0, 0, 0, time.UTC)
queuedText := strings.Repeat("queued ", maxHistoryMessageBytes)
for _, event := range []Event{
{Type: EventFileDiff, Diff: "diff --git a/old.txt b/old.txt\n-old\n+new"},
{Type: EventUserMessage, TurnID: "turn-1", Text: "active request"},
{Type: EventTurnStarted, TurnID: "turn-1", Timestamp: &startedAt, Status: "running"},
{Type: EventAssistantDelta, TurnID: "turn-1", Text: "partial answer"},
Expand Down Expand Up @@ -1659,6 +1660,9 @@ func TestServerPreservesStateOutsideProjectedHistoryPage(t *testing.T) {
if len(state.QueuedTurns) != 1 || state.QueuedTurns[0].TurnID != "turn-2" {
t.Fatalf("queued turns = %#v, want turn-2", state.QueuedTurns)
}
if state.FileDiff != "diff --git a/old.txt b/old.txt\n-old\n+new" {
t.Fatalf("file diff = %q, want state outside projected history page", state.FileDiff)
}
if text := state.QueuedTurns[0].Text; len(text) > maxHistoryMessageBytes || !strings.Contains(text, historyTruncationMarker) {
t.Fatalf("queued message preview has %d bytes", len(text))
}
Expand Down
27 changes: 15 additions & 12 deletions internal/sessionserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,21 @@ func TestSessionJavaScriptCachesIncrementalHistory(t *testing.T) {
}
javascript := string(source)
for description, expected := range map[string]string{
"bounded Session view cache": "const maxCachedSessionViews = 5;",
"Session incarnation cache key": "function sessionViewKey(session) {",
"recreated Session invalidation": "state.selected.uid !== current.uid",
"view state preservation": "function saveCurrentSessionView() {",
"incremental history subscription": "historyBounds: true",
"journal identity subscription": "journalId: state.currentView?.journalID || '',",
"stale history reset": "if (event.reset ||",
"replaced journal reset": "state.currentView.journalID !== event.journalId",
"deferred history rendering": "function finishHistoryReplay() {",
"selection bottom pin": "state.pinHistoryToBottom = true;",
"frame-batched bottom anchor": "function scheduleBottomAnchor() {",
"instant bottom positioning": "elements.messages.scrollTop = elements.messages.scrollHeight;",
"bounded Session view cache": "const maxCachedSessionViews = 5;",
"Session incarnation cache key": "function sessionViewKey(session) {",
"recreated Session invalidation": "state.selected.uid !== current.uid",
"view state preservation": "function saveCurrentSessionView() {",
"bounded history item subscription": "historyItems: sessionHistoryItemLimit",
"bounded history byte subscription": "historyBytes: sessionHistoryByteLimit",
"history cursor preservation": "view.historyCursor = state.historyCursor",
"reconnect high-water tracking": "state.lastEventID = Math.max(state.lastEventID, state.historyLastEventID)",
"older history request guard": "if (state.historyPageLoading || !state.historyCursor",
"older history isolation": "function replayOlderHistoryPage(events) {",
"replaced journal reset": "state.currentView.journalID !== event.journalId",
"deferred history rendering": "function finishHistoryReplay(historyState) {",
"selection bottom pin": "state.pinHistoryToBottom = true;",
"frame-batched bottom anchor": "function scheduleBottomAnchor() {",
"instant bottom positioning": "elements.messages.scrollTop = elements.messages.scrollHeight;",
} {
if !strings.Contains(javascript, expected) {
t.Errorf("Session JavaScript is missing %s: %s", description, expected)
Expand Down
Loading
Loading