Skip to content

Commit 75e9f55

Browse files
author
SqlRush
committed
Accept provider remote history pages
1 parent d51984c commit 75e9f55

5 files changed

Lines changed: 179 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ M6 补充:remote history event-list 现在接受 `value`/`values`/`resources`/
120120

121121
M6 补充:remote history response parser 现在会解包 `payload`/`response`/`result`/`body` 等通用响应外壳,外壳内的 event list、pagination、links 会继续递归解析。
122122

123+
M6 补充:remote history response parser 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 event page JSON,并保留 pagination 继续驱动 `before_id` 续抓。
124+
123125
M6 补充:remote history response parser 现在接受 JSON:API `included` collection,会过滤非事件资源,并递归解包 `resource`/`attributes`/`properties` 后用外层 resource id 作为事件 ID fallback。
124126

125127
M6 补充:remote history `data`/`payload`/`response`/`result`/`body` 等 event-list 字段现在也接受单个 SDK event 对象,避免非数组单事件页被当作普通 wrapper 后丢失。
@@ -526,6 +528,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
526528

527529
本轮补充:compact runner 的 summary 响应现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可在构建 compact plan 前从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 visible summary text。
528530

531+
本轮补充:remote history response parser 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 event page JSON,并保留 pagination 继续驱动 `before_id` 续抓。
532+
529533
本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase、snake_case 和相邻实现常见字段别名/包装形态,包括 `result`/`data`/`cache`/`value` wrapper、`content`/`text` summary、`cacheKey`/`key`/`hash` digest、cache-hit 别名、计数字段别名/数字字符串、RFC3339/Unix 秒/Unix 毫秒时间字段,以及 `createdAt` + `ttlSeconds`/`ttlMs`/`expiresIn`/`maxAge` 等相对 TTL 推导,提升 cached microcompact 文件在不同实现/版本间的恢复率。
530534

531535
本轮补充:microcompact disk cache loader 现在也接受 `cacheEntry`/`cache_entry``micro_compact`/`micro_compact_result` wrapper,以及 `summaryMarkdown`/`resultSummary`/`compressedText``cacheDigest`/`digestHash`/`fingerprint``summarizedCount`/`retainedCount``formatVersion``ttlMilliseconds`/`expiresInMilliseconds`/`maxAgeMilliseconds` 等相邻实现字段别名。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ test/parity/ # golden tests against TS/official behavior
214214
- 本轮补充:remote history 在 JSON:API `relationships.events.data` 只有 resource identifier 时,会继续使用 top-level `included` 中的真实事件 resource,避免把 `{type,id}` 标识符误当作空事件遮蔽完整 payload。
215215
- 本轮补充:remote history response parser 现在也接受 JSON:API `included` collection,会过滤非事件资源,并递归解包 `resource`/`attributes`/`properties` 后保留外层 resource id 作为事件 ID fallback。
216216
- 本轮补充:remote history response parser 会解包 `payload`/`response`/`result`/`body` 等通用响应外壳,外壳内的 event list、pagination、links 会继续递归解析。
217+
- 本轮补充:remote history response parser 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 中恢复 event page JSON,并保留 pagination 继续驱动 `before_id` 续抓。
217218
- 本轮补充:remote history `SDKEvent` 本体接受 `eventType`/`event_type`/`role` 类型别名、`createdAt`/`created_at` 时间戳别名,以及 `payload`/`data`/`body`/`serializedMessage` message payload 别名;payload 只有 `role`/`content` 时也能 materialize 成 transcript message。
218219
- 本轮补充:transcript resume 的嵌套 content block 接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
219220
- 本轮补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ M6 progress now includes:
143143
- `internal/memory`: session-memory recall and relevant-memory model selectors now recurse through GraphQL/JSON:API selection wrappers such as `data`, `payload`, `body`, `resource`, `attributes`, `properties`, `attrs`, `viewer`, `edge`, `node`, `nodes`, and `edges`, preserving model-ranked session IDs and memory paths from API-shaped responses.
144144
- `internal/memory`: session-memory recall and relevant-memory model selectors now also unwrap provider-style `choices`, `outputs`, `candidates`, and `generations` responses, recovering JSON selection payloads from nested `message.content`, content-block arrays, and `content.parts[].text`.
145145
- `internal/memory`: model-backed memory fact extraction now also unwraps provider-style `choices`, `outputs`, `candidates`, and `generations` responses, recovering JSON facts payloads from nested `message.content`, content-block arrays, and `content.parts[].text`.
146+
- `internal/session`: remote-history response parsing now also unwraps provider-style `choices`, `outputs`, `candidates`, and `generations` responses, recovering event page JSON plus pagination from nested `message.content`, content-block arrays, and `content.parts[].text`.
146147
- `internal/session`: project session listing and pagination, prompt-history lock/buffered flush/field aliases, lightweight transcript index/title/text-preview inference, line-offset transcript indexing/window/byte-budget-window/parent-chain/resume/tail/byte-budget-tail loading, AI-title/last-prompt/task/agent/PR/worktree transcript metadata loading and type/field aliases, transcript message/session UUID field aliases, tombstone metadata loading with target/session/reason aliases and delete/relink replay, agent-scoped content replacement metadata/record field-alias loading, session-scoped metadata reappend including AI-title/last-prompt/task-summary, streaming transcript search snippets for resume/search UI, official `subagents/agent-*.jsonl` transcript layout with legacy sidechain listing, agent metadata sidecar read/write/field aliases, sidechain runtime start/append/finish/cancel/fail summary bridging plus parent-chain append/finish, sidechain manager orchestration for spawn/append/finish/cancel/fail/list/resume, sidechain state/list/resume support with content-field aliases, sidechain resume context construction, sidechain conversation and agent-scoped content-replacement reconstruction, transcript tail/window loading and byte-budget tail loading for bounded-memory resume/UI paths, lightweight transcript metadata loading, remote-history token refresh retry, page-field/event-list/records/entries/last-id/cursor/event-id/has-next aliases plus wrapped-data/links/paging/bare-array/keyed-event-map responses, before_id resume, fallback field fill, and duplicate-aware parent linking during transcript materialization.
147148
- `internal/session`: transcript full loader and line-offset index now accept top-level record UUID aliases such as `messageUuid`, `messageUUID`, `message_id`, `messageId`, and `id`, plus parent message UUID/ID aliases for progress bridge and indexed resume paths.
148149
- `internal/session`: indexed resume chains now distinguish budget-truncated parents from genuinely missing parents, exposing both markers while preserving bounded transcript reads.

internal/session/remote_history.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ func (r *sessionEventsResponse) mergeJSON(data []byte) error {
219219
r.Included = events
220220
}
221221
}
222+
if responseEventList(*r) == nil {
223+
if text, ok := remoteHistoryProviderResponseText(raw); ok {
224+
var nested sessionEventsResponse
225+
if err := nested.mergeJSON([]byte(text)); err != nil {
226+
return fmt.Errorf("remote history provider response: %w", err)
227+
}
228+
r.mergeFrom(nested)
229+
}
230+
}
222231
return nil
223232
}
224233

@@ -1015,6 +1024,101 @@ func remoteHistoryRelTokens(raw map[string]json.RawMessage) []string {
10151024
return tokens
10161025
}
10171026

1027+
func remoteHistoryProviderResponseText(raw map[string]json.RawMessage) (string, bool) {
1028+
for _, name := range []string{
1029+
"choice",
1030+
"choices",
1031+
"output",
1032+
"outputs",
1033+
"candidate",
1034+
"candidates",
1035+
"generation",
1036+
"generations",
1037+
"completion",
1038+
"completions",
1039+
"response",
1040+
"responses",
1041+
"result",
1042+
"results",
1043+
} {
1044+
value, ok := raw[name]
1045+
if !ok {
1046+
continue
1047+
}
1048+
if text, ok := remoteHistoryProviderTextFromRaw(value, 0, false); ok {
1049+
return text, true
1050+
}
1051+
}
1052+
return "", false
1053+
}
1054+
1055+
func remoteHistoryProviderTextFromRaw(raw json.RawMessage, depth int, allowScalar bool) (string, bool) {
1056+
raw = bytes.TrimSpace(raw)
1057+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || depth > 8 {
1058+
return "", false
1059+
}
1060+
var text string
1061+
if err := json.Unmarshal(raw, &text); err == nil {
1062+
text = strings.TrimSpace(text)
1063+
if allowScalar && remoteHistoryProviderTextLooksJSON(text) {
1064+
return text, true
1065+
}
1066+
return "", false
1067+
}
1068+
if raw[0] == '[' {
1069+
var items []json.RawMessage
1070+
if err := json.Unmarshal(raw, &items); err != nil {
1071+
return "", false
1072+
}
1073+
parts := make([]string, 0, len(items))
1074+
for _, item := range items {
1075+
part, ok := remoteHistoryProviderTextFromRaw(item, depth+1, false)
1076+
if ok {
1077+
parts = append(parts, part)
1078+
}
1079+
}
1080+
if len(parts) == 0 {
1081+
return "", false
1082+
}
1083+
text = strings.Join(parts, "\n")
1084+
if remoteHistoryProviderTextLooksJSON(text) {
1085+
return text, true
1086+
}
1087+
return "", false
1088+
}
1089+
if raw[0] != '{' {
1090+
return "", false
1091+
}
1092+
fields := map[string]json.RawMessage{}
1093+
if err := json.Unmarshal(raw, &fields); err != nil {
1094+
return "", false
1095+
}
1096+
for _, name := range []string{"text", "content", "value", "output"} {
1097+
value, ok := fields[name]
1098+
if !ok {
1099+
continue
1100+
}
1101+
if text, ok := remoteHistoryProviderTextFromRaw(value, depth+1, true); ok {
1102+
return text, true
1103+
}
1104+
}
1105+
for _, name := range []string{"message", "delta", "part", "parts", "candidate", "choice", "generation", "result", "response"} {
1106+
value, ok := fields[name]
1107+
if !ok {
1108+
continue
1109+
}
1110+
if text, ok := remoteHistoryProviderTextFromRaw(value, depth+1, false); ok {
1111+
return text, true
1112+
}
1113+
}
1114+
return "", false
1115+
}
1116+
1117+
func remoteHistoryProviderTextLooksJSON(text string) bool {
1118+
text = strings.TrimSpace(text)
1119+
return strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[")
1120+
}
1121+
10181122
func jsonNumberString(data []byte) (string, bool) {
10191123
decoder := json.NewDecoder(bytes.NewReader(data))
10201124
decoder.UseNumber()

internal/session/remote_history_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,75 @@ func TestFetchRemoteHistoryAcceptsGraphQLViewerAndNodeWrappers(t *testing.T) {
528528
}
529529
}
530530

531+
func TestFetchRemoteHistoryAcceptsProviderResponseWrappers(t *testing.T) {
532+
providerWrapper := func(t *testing.T, field string, page map[string]any) []byte {
533+
t.Helper()
534+
pageData, err := json.Marshal(page)
535+
if err != nil {
536+
t.Fatal(err)
537+
}
538+
var wrapper map[string]any
539+
switch field {
540+
case "choices":
541+
wrapper = map[string]any{
542+
"choices": []any{map[string]any{
543+
"message": map[string]any{"content": string(pageData)},
544+
}},
545+
}
546+
case "candidates":
547+
wrapper = map[string]any{
548+
"candidates": []any{map[string]any{
549+
"content": map[string]any{
550+
"parts": []any{map[string]any{"text": string(pageData)}},
551+
},
552+
}},
553+
}
554+
default:
555+
t.Fatalf("unknown provider field %q", field)
556+
}
557+
data, err := json.Marshal(wrapper)
558+
if err != nil {
559+
t.Fatal(err)
560+
}
561+
return data
562+
}
563+
564+
var seen []url.Values
565+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
566+
seen = append(seen, r.URL.Query())
567+
w.Header().Set("Content-Type", "application/json")
568+
switch r.URL.Query().Get("before_id") {
569+
case "":
570+
_, _ = w.Write(providerWrapper(t, "choices", map[string]any{
571+
"events": []any{map[string]any{"type": "status", "event_id": "evt_provider", "session_id": "s", "status": "latest"}},
572+
"hasMore": true,
573+
}))
574+
case "evt_provider":
575+
_, _ = w.Write(providerWrapper(t, "candidates", map[string]any{
576+
"items": []any{map[string]any{"type": "status", "eventId": "evt_candidate", "sessionId": "s", "status": "older"}},
577+
}))
578+
default:
579+
t.Fatalf("unexpected before_id = %q", r.URL.Query().Get("before_id"))
580+
}
581+
}))
582+
defer server.Close()
583+
584+
authCtx := NewRemoteHistoryAuthContext("s", "token", "", auth.OAuthConfig{BaseAPIURL: server.URL})
585+
events, err := FetchRemoteHistory(context.Background(), server.Client(), authCtx, RemoteHistoryFetchOptions{Limit: 2})
586+
if err != nil {
587+
t.Fatal(err)
588+
}
589+
if !events.Complete || events.Pages != 2 || events.NextBeforeID != "" {
590+
t.Fatalf("events = %#v", events)
591+
}
592+
if len(events.Events) != 2 || events.Events[0].ID != "evt_provider" || events.Events[0].Status != "latest" || events.Events[1].ID != "evt_candidate" || events.Events[1].Status != "older" {
593+
t.Fatalf("event order = %#v", events.Events)
594+
}
595+
if len(seen) != 2 || seen[1].Get("before_id") != "evt_provider" {
596+
t.Fatalf("queries = %#v", seen)
597+
}
598+
}
599+
531600
func TestFetchRemoteHistoryAcceptsLinkURLCursors(t *testing.T) {
532601
var seen []url.Values
533602
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)