Skip to content

Commit 564ab62

Browse files
author
SqlRush
committed
Accept empty remote history pages
1 parent 3f4dddc commit 564ab62

5 files changed

Lines changed: 41 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ M6 补充:remote history pagination 现在接受 `nextPageToken`/`nextToken`/`
7272

7373
M6 补充:remote history pagination 现在也接受 OData next-link 字段 `@odata.nextLink``odata.nextLink``__next`,并从 `$skiptoken`/`skipToken` link query 参数提取续抓 cursor。
7474

75+
M6 补充:remote history fetch 现在把 HTTP 204 和 200 空 body 视为空的终止页,避免空历史响应被标成 incomplete 或触发 JSON EOF。
76+
7577
M6 补充:contract `ID` JSON 读取现在接受 JSON number/null,remote history event/message/session/parent ID alias 可继承数字 ID 兼容面并在 transcript materialization 中保留为字符串。
7678

7779
M6 补充:remote history response parser 现在会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory``_embedded` 等 GraphQL/session/HAL wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ test/parity/ # golden tests against TS/official behavior
195195
- 本轮补充:remote history pagination cursor/id 字段现在接受 JSON number 并原样转成字符串,覆盖 `next_cursor` 等 page 字段和 `edges[].cursor` 的数字形态。
196196
- 本轮补充:remote history pagination 现在接受 `nextPageToken`/`nextToken`/`pageToken`/`continuationToken` 及 snake_case 形式,响应字段和 link URL query 参数都会归一到续抓 before-id。
197197
- 本轮补充:remote history pagination 现在也接受 OData next-link 字段 `@odata.nextLink``odata.nextLink``__next`,并从 `$skiptoken`/`skipToken` link query 参数提取续抓 cursor。
198+
- 本轮补充:remote history fetch 把 HTTP 204 和 200 空 body 视为空的终止页,避免空历史响应被标成 incomplete 或触发 JSON EOF。
198199
- 本轮补充:contract `ID` JSON 读取现在接受 JSON number/null,remote history event/message/session/parent ID alias 可继承数字 ID 兼容面并在 transcript materialization 中保留为字符串。
199200
- 本轮补充:remote history response parser 会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory``_embedded` 等 GraphQL/session/HAL wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。
200201
- 本轮补充:remote history event-list 接受 `value`/`values`/`resources`/`collection` 别名,connection edge 也接受 `resource`/`value` 作为 node payload,覆盖 OData/HAL/resource collection 风格响应。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ M6 progress now includes:
151151
- `internal/session`: remote-history pagination cursor/id parsing now accepts JSON numbers and preserves them as string cursors for page fields and `edges[].cursor`.
152152
- `internal/session`: remote-history pagination now accepts token aliases such as `nextPageToken`, `nextToken`, `pageToken`, and `continuationToken` in response fields and link query parameters.
153153
- `internal/session`: remote-history pagination now also accepts OData next-link fields such as `@odata.nextLink`, `odata.nextLink`, and `__next`, extracting `$skiptoken`/`skipToken` link query values as continuation cursors.
154+
- `internal/session`: remote-history fetch now treats HTTP 204 and 200 responses with empty bodies as empty terminal pages instead of incomplete fetches or JSON EOF errors.
154155
- `internal/contracts`: ID JSON decoding now accepts JSON numbers/null so remote-history event, message, session, and parent ID aliases can preserve numeric IDs as strings during transcript materialization.
155156
- `internal/session`: remote-history response parsing now recursively unwraps GraphQL/session/HAL containers such as `data.session.events`, `data.projectSession.eventConnection`, `conversation`, `remoteHistory`, and `_embedded` before applying `nodes`/`edges[].node` event-list and `pageInfo` pagination parsing.
156157
- `internal/session`: remote-history link pagination now accepts `links`/`_links` `next`/`previous`/`prev`/`older` string URLs or `{href,url,uri,link}` objects and extracts before/cursor query parameters for continuation.

internal/session/remote_history.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"net/http"
910
"net/url"
1011
"strconv"
@@ -857,11 +858,17 @@ func fetchRemoteHistoryPageStatus(ctx context.Context, client *http.Client, auth
857858
return nil, 0, nil
858859
}
859860
defer resp.Body.Close()
861+
if resp.StatusCode == http.StatusNoContent {
862+
return &RemoteHistoryPage{Events: []contracts.SDKEvent{}}, resp.StatusCode, nil
863+
}
860864
if resp.StatusCode != http.StatusOK {
861865
return nil, resp.StatusCode, nil
862866
}
863867
var decoded sessionEventsResponse
864868
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
869+
if err == io.EOF {
870+
return &RemoteHistoryPage{Events: []contracts.SDKEvent{}}, resp.StatusCode, nil
871+
}
865872
return nil, resp.StatusCode, err
866873
}
867874
events := responseEventList(decoded)

internal/session/remote_history_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,36 @@ func TestFetchRemoteHistoryNonOKReturnsNil(t *testing.T) {
108108
}
109109
}
110110

111+
func TestFetchRemoteHistoryAcceptsEmptyTerminalPages(t *testing.T) {
112+
for _, tc := range []struct {
113+
name string
114+
serve func(http.ResponseWriter)
115+
}{
116+
{name: "no-content", serve: func(w http.ResponseWriter) {
117+
w.WriteHeader(http.StatusNoContent)
118+
}},
119+
{name: "empty-ok", serve: func(w http.ResponseWriter) {
120+
w.Header().Set("Content-Type", "application/json")
121+
}},
122+
} {
123+
t.Run(tc.name, func(t *testing.T) {
124+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
125+
tc.serve(w)
126+
}))
127+
defer server.Close()
128+
129+
authCtx := NewRemoteHistoryAuthContext("s", "", "", auth.OAuthConfig{BaseAPIURL: server.URL})
130+
events, err := FetchRemoteHistory(context.Background(), server.Client(), authCtx, RemoteHistoryFetchOptions{Limit: 1})
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
if !events.Complete || events.Pages != 1 || len(events.Events) != 0 || events.NextBeforeID != "" {
135+
t.Fatalf("events = %#v", events)
136+
}
137+
})
138+
}
139+
}
140+
111141
func TestFetchRemoteHistoryRefreshesTokenOnUnauthorized(t *testing.T) {
112142
var tokens []string
113143
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)