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
14 changes: 6 additions & 8 deletions crates/agent-gateway/internal/server/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ type websocketConnection struct {

const recentActiveChatRetention = 5 * time.Second
const maxHistoryListLimit = 200
const defaultHistoryListPage = 1
const defaultHistoryListPageSize = 80

func NewWebSocketServer(cfg *config.Config, sm *session.Manager) http.Handler {
server := &websocket.Server{
Expand Down Expand Up @@ -500,13 +502,11 @@ func (c *websocketConnection) handleHistoryList(req websocketRequest) {
}
page := body.Page
if page <= 0 {
_ = c.writeError(req.ID, "history.list page must be greater than 0")
return
page = defaultHistoryListPage
}
pageSize := body.PageSize
if pageSize <= 0 {
_ = c.writeError(req.ID, "history.list page_size must be greater than 0")
return
pageSize = defaultHistoryListPageSize
} else if pageSize > maxHistoryListLimit {
pageSize = maxHistoryListLimit
}
Expand Down Expand Up @@ -561,13 +561,11 @@ func (c *websocketConnection) handleHistorySharedList(req websocketRequest) {
}
page := body.Page
if page <= 0 {
_ = c.writeError(req.ID, "history.shared_list page must be greater than 0")
return
page = defaultHistoryListPage
}
pageSize := body.PageSize
if pageSize <= 0 {
_ = c.writeError(req.ID, "history.shared_list page_size must be greater than 0")
return
pageSize = defaultHistoryListPageSize
} else if pageSize > maxHistoryListLimit {
pageSize = maxHistoryListLimit
}
Expand Down
69 changes: 69 additions & 0 deletions crates/agent-gateway/test/websocket/chat_bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1267,3 +1267,72 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
t.Fatalf("preview payload = %#v", previewPayload)
}
}

func TestWebSocketDefaultsInvalidHistoryListPagination(t *testing.T) {
t.Parallel()

sm := session.NewManager()
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)

handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
RequestTimeout: time.Second,
}, sm)
conn, cleanup := dialGatewayWebSocket(t, handler)
defer cleanup()

authWebSocket(t, conn, "ws-token")

sendEnvelope(t, conn, "history-defaults", "history.list", map[string]any{
"page": 0,
"page_size": 0,
})
historyOutbound := readOutboundEnvelope(t, agentSession)
historyReq := historyOutbound.GetHistoryList()
if historyReq == nil {
t.Fatalf("history outbound payload = %T, want HistoryListRequest", historyOutbound.GetPayload())
}
if historyReq.GetPage() != 1 || historyReq.GetPageSize() != 80 {
t.Fatalf("history list defaults = %#v", historyReq)
}
sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
RequestId: historyOutbound.GetRequestId(),
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.AgentEnvelope_HistoryListResp{
HistoryListResp: &gatewayv1.HistoryListResponse{},
},
})
historyResponse := receiveEnvelope(t, conn)
if historyResponse.ID != "history-defaults" || historyResponse.Type != "response" {
t.Fatalf("history response = %#v", historyResponse)
}

sendEnvelope(t, conn, "shared-history-defaults", "history.shared_list", map[string]any{})
sharedHistoryOutbound := readOutboundEnvelope(t, agentSession)
sharedHistoryReq := sharedHistoryOutbound.GetMemoryManage()
if sharedHistoryReq == nil {
t.Fatalf("shared history outbound payload = %T, want MemoryManageRequest", sharedHistoryOutbound.GetPayload())
}
var sharedHistoryArgs map[string]any
if err := json.Unmarshal([]byte(sharedHistoryReq.GetArgsJson()), &sharedHistoryArgs); err != nil {
t.Fatalf("decode shared history args: %v", err)
}
if sharedHistoryArgs["page"] != float64(1) || sharedHistoryArgs["page_size"] != float64(80) {
t.Fatalf("shared history defaults = %#v", sharedHistoryArgs)
}
sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
RequestId: sharedHistoryOutbound.GetRequestId(),
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.AgentEnvelope_MemoryManageResp{
MemoryManageResp: &gatewayv1.MemoryManageResponse{
ResultJson: `{"total_count":0,"conversations":[]}`,
},
},
})
sharedHistoryResponse := receiveEnvelope(t, conn)
if sharedHistoryResponse.ID != "shared-history-defaults" || sharedHistoryResponse.Type != "response" {
t.Fatalf("shared history response = %#v", sharedHistoryResponse)
}
}
37 changes: 37 additions & 0 deletions crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,43 @@ test("GatewayWebSocketClient sends history list requests", async () => {
resetGatewayWebSocketClient();
});

test("GatewayWebSocketClient defaults invalid history pagination", async () => {
installBrowser();
const loader = createWebModuleLoader();
const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule("src/lib/gatewaySocket.ts");
resetGatewayWebSocketClient();

const client = getGatewayWebSocketClient("token");
const listPromise = client.listHistory(0, 0);
const socket = await connectAndAuth();
await waitFor(() => socket.sent.length >= 2, "history list envelope");
assert.equal(socket.sent[1].type, "history.list");
assert.deepEqual(socket.sent[1].payload, { page: 1, page_size: 80 });
socket.receive({
id: socket.sent[1].id,
type: "response",
payload: { conversations: [], total_count: 0, running_conversation_ids: [] },
});
assert.deepEqual(await listPromise, {
conversations: [],
total_count: 0,
running_conversation_ids: [],
});

const sharedListPromise = client.listSharedHistory(Number.NaN, 500);
await waitFor(() => socket.sent.length >= 3, "shared history list envelope");
assert.equal(socket.sent[2].type, "history.shared_list");
assert.deepEqual(socket.sent[2].payload, { page: 1, page_size: 200 });
socket.receive({
id: socket.sent[2].id,
type: "response",
payload: { conversations: [], total_count: 0 },
});
assert.deepEqual(await sharedListPromise, { conversations: [], total_count: 0 });

resetGatewayWebSocketClient();
});

test("GatewayWebSocketClient sends history share requests", async () => {
installBrowser();
const loader = createWebModuleLoader();
Expand Down
41 changes: 35 additions & 6 deletions crates/agent-gateway/web/src/lib/gatewaySocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ type RuntimeHost = {
clearInterval: typeof clearInterval;
};

const DEFAULT_HISTORY_LIST_PAGE = 1;
const DEFAULT_HISTORY_LIST_PAGE_SIZE = 80;
const MAX_HISTORY_LIST_PAGE_SIZE = 200;

function getRuntimeHost(): RuntimeHost {
if (typeof window !== "undefined") {
return window as unknown as RuntimeHost;
Expand Down Expand Up @@ -252,6 +256,25 @@ function normalizeAfterSeq(value: unknown) {
: 0;
}

function normalizePositiveInteger(value: number, fallback: number) {
if (!Number.isFinite(value)) {
return fallback;
}
const normalized = Math.trunc(value);
return normalized > 0 ? normalized : fallback;
}

function normalizeHistoryListPage(page: number) {
return normalizePositiveInteger(page, DEFAULT_HISTORY_LIST_PAGE);
}

function normalizeHistoryListPageSize(pageSize: number) {
return Math.min(
normalizePositiveInteger(pageSize, DEFAULT_HISTORY_LIST_PAGE_SIZE),
MAX_HISTORY_LIST_PAGE_SIZE,
);
}

function isRecoverableGatewayTransportError(error: unknown) {
const message = asErrorMessage(error, "");
return (
Expand Down Expand Up @@ -539,15 +562,15 @@ export class GatewayWebSocketClient {

async listHistory(page: number, pageSize: number): Promise<HistoryList> {
return this.requestWithRecovery<HistoryList>("history.list", {
page,
page_size: pageSize,
page: normalizeHistoryListPage(page),
page_size: normalizeHistoryListPageSize(pageSize),
});
}

async listSharedHistory(page: number, pageSize: number): Promise<HistoryList> {
return this.requestWithRecovery<HistoryList>("history.shared_list", {
page,
page_size: pageSize,
page: normalizeHistoryListPage(page),
page_size: normalizeHistoryListPageSize(pageSize),
});
}

Expand Down Expand Up @@ -1805,11 +1828,17 @@ class SharedWorkerGatewayWebSocketClient implements GatewayWebSocketClientLike {
}

async listHistory(page: number, pageSize: number): Promise<HistoryList> {
return this.request<HistoryList>("history.list", { page, page_size: pageSize });
return this.request<HistoryList>("history.list", {
page: normalizeHistoryListPage(page),
page_size: normalizeHistoryListPageSize(pageSize),
});
}

async listSharedHistory(page: number, pageSize: number): Promise<HistoryList> {
return this.request<HistoryList>("history.shared_list", { page, page_size: pageSize });
return this.request<HistoryList>("history.shared_list", {
page: normalizeHistoryListPage(page),
page_size: normalizeHistoryListPageSize(pageSize),
});
}

async getHistory(conversationId: string, options?: HistoryGetOptions): Promise<HistoryDetail> {
Expand Down
15 changes: 13 additions & 2 deletions crates/agent-gui/src-tauri/src/services/gateway_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use crate::services::skills::system_manage_skill_sync;

const DEFAULT_FS_LIST_DIRS_MAX_RESULTS: usize = 2000;
const HARD_FS_LIST_DIRS_MAX_RESULTS: usize = 10000;
const DEFAULT_HISTORY_LIST_PAGE: i32 = 1;
const DEFAULT_HISTORY_LIST_PAGE_SIZE: i32 = 80;

#[derive(Debug, Deserialize)]
struct HistorySharedListArgs {
Expand Down Expand Up @@ -91,9 +93,18 @@ pub async fn handle_cron_manage(
pub async fn handle_history_list(
request: proto::HistoryListRequest,
) -> Result<proto::HistoryListResponse, String> {
let page_number = if request.page > 0 {
request.page
} else {
DEFAULT_HISTORY_LIST_PAGE
};
let page_size = if request.page_size > 0 {
request.page_size
} else {
DEFAULT_HISTORY_LIST_PAGE_SIZE
};
let page =
chat_history::chat_history_list(i64::from(request.page), i64::from(request.page_size))
.await?;
chat_history::chat_history_list(i64::from(page_number), i64::from(page_size)).await?;
Ok(build_proto_history_list_response(page))
}

Expand Down
Loading