Skip to content

Commit 5f062a8

Browse files
committed
fix(web-session): 修正 Codex 深度回读历史投影
过滤内部协议与注入上下文,并合并 event/response 重复消息、图片附件和计划记录。
1 parent 6f0c4bc commit 5f062a8

2 files changed

Lines changed: 352 additions & 30 deletions

File tree

service/websession/sync_deep.go

Lines changed: 267 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9+
"regexp"
910
"strings"
1011
"time"
1112

@@ -45,6 +46,16 @@ type codexDeepHistoryParseResult struct {
4546
Stats codexDeepHistoryStats
4647
}
4748

49+
type codexDeepMessageOrigin uint8
50+
51+
const (
52+
codexDeepMessageOriginEvent codexDeepMessageOrigin = 1 << iota
53+
codexDeepMessageOriginResponse
54+
codexDeepMessageDedupeWindow = 2 * time.Second
55+
)
56+
57+
var codexEmbeddedImageEnvelopePattern = regexp.MustCompile(`(?s)[\r\n]*<image\b[^>]*\bpath="([^"]+)"[^>]*>\s*\[input_image:\]\s*</image>`)
58+
4859
func (m *Manager) syncSessionFromLogSource(
4960
ctx context.Context,
5061
session tables.WebSessionTable,
@@ -185,6 +196,7 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
185196
items := make([]HistoryItem, 0, 256)
186197
pendingTools := make(map[string]int)
187198
pendingUserInputs := make(map[string]int)
199+
messageOrigins := make(map[string]codexDeepMessageOrigin)
188200
var orderIndex int64
189201
var stats codexDeepHistoryStats
190202

@@ -202,6 +214,30 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
202214
return len(items) - 1
203215
}
204216

217+
appendMessage := func(item HistoryItem, origin codexDeepMessageOrigin) int {
218+
itemTime := codexDeepMessageTime(item)
219+
for index := len(items) - 1; index >= 0; index-- {
220+
candidate := items[index]
221+
candidateTime := codexDeepMessageTime(candidate)
222+
if itemTime != nil && candidateTime != nil && itemTime.Sub(*candidateTime) > codexDeepMessageDedupeWindow {
223+
break
224+
}
225+
candidateOrigin := messageOrigins[candidate.ID]
226+
if candidateOrigin == 0 || candidateOrigin&origin != 0 {
227+
continue
228+
}
229+
if !codexDeepMessagesMatch(candidate, item) {
230+
continue
231+
}
232+
mergeCodexDeepMessage(&items[index], item, candidateOrigin, origin)
233+
messageOrigins[candidate.ID] = candidateOrigin | origin
234+
return index
235+
}
236+
index := appendItem(item)
237+
messageOrigins[item.ID] = origin
238+
return index
239+
}
240+
205241
appendIfNotDuplicate := func(item HistoryItem) {
206242
if item.ItemType == "context_compaction" && len(items) > 0 {
207243
last := items[len(items)-1]
@@ -242,6 +278,10 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
242278
}
243279
stats.observeEventMessage(payload, ts)
244280
for _, item := range m.codexHistoryItemsFromEventMessage(payload, ts) {
281+
if isCodexDeepMessage(item) {
282+
appendMessage(item, codexDeepMessageOriginEvent)
283+
continue
284+
}
245285
appendIfNotDuplicate(item)
246286
}
247287
case "response_item":
@@ -257,6 +297,7 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
257297
payload,
258298
ts,
259299
appendItem,
300+
appendMessage,
260301
pendingTools,
261302
pendingUserInputs,
262303
)
@@ -266,6 +307,7 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
266307
return codexDeepHistoryParseResult{}, err
267308
}
268309

310+
items = dedupeCodexDeepPlanMessages(items)
269311
for index := range items {
270312
items[index].OrderIndex = int64(index + 1)
271313
}
@@ -275,6 +317,178 @@ func (m *Manager) parseCodexDeepHistoryWithStats(filePath string) (codexDeepHist
275317
}, nil
276318
}
277319

320+
func isCodexDeepMessage(item HistoryItem) bool {
321+
return item.Tool == nil && (item.Kind == "user" || item.Kind == "assistant")
322+
}
323+
324+
func normalizedCodexDeepMessageText(text string) string {
325+
return strings.TrimSpace(strings.ReplaceAll(text, "\r\n", "\n"))
326+
}
327+
328+
func codexDeepMessageTime(item HistoryItem) *time.Time {
329+
if item.Timestamp != nil {
330+
return item.Timestamp
331+
}
332+
return item.ObservedAt
333+
}
334+
335+
func codexDeepMessageAttachmentsCompatible(left, right []HistoryAttachment) bool {
336+
if len(left) == 0 || len(right) == 0 {
337+
return true
338+
}
339+
if len(left) != len(right) {
340+
return false
341+
}
342+
keys := make(map[string]struct{}, len(left))
343+
for _, attachment := range left {
344+
key := firstNonEmpty(strings.TrimSpace(attachment.ID), strings.TrimSpace(attachment.Path))
345+
keys[key] = struct{}{}
346+
}
347+
for _, attachment := range right {
348+
key := firstNonEmpty(strings.TrimSpace(attachment.ID), strings.TrimSpace(attachment.Path))
349+
if _, ok := keys[key]; !ok {
350+
return false
351+
}
352+
}
353+
return true
354+
}
355+
356+
func codexDeepMessagesMatch(left, right HistoryItem) bool {
357+
if !isCodexDeepMessage(left) || !isCodexDeepMessage(right) || left.Kind != right.Kind {
358+
return false
359+
}
360+
if normalizedCodexDeepMessageText(left.Text) != normalizedCodexDeepMessageText(right.Text) {
361+
return false
362+
}
363+
if !codexDeepMessageAttachmentsCompatible(left.Attachments, right.Attachments) {
364+
return false
365+
}
366+
leftTime := codexDeepMessageTime(left)
367+
rightTime := codexDeepMessageTime(right)
368+
if leftTime == nil || rightTime == nil {
369+
return false
370+
}
371+
delta := leftTime.Sub(*rightTime)
372+
if delta < 0 {
373+
delta = -delta
374+
}
375+
return delta <= codexDeepMessageDedupeWindow
376+
}
377+
378+
func mergeHistoryAttachments(left, right []HistoryAttachment) []HistoryAttachment {
379+
if len(left) == 0 {
380+
return right
381+
}
382+
if len(right) == 0 {
383+
return left
384+
}
385+
result := append([]HistoryAttachment{}, left...)
386+
seen := make(map[string]struct{}, len(result))
387+
for _, attachment := range result {
388+
key := firstNonEmpty(strings.TrimSpace(attachment.ID), strings.TrimSpace(attachment.Path))
389+
seen[key] = struct{}{}
390+
}
391+
for _, attachment := range right {
392+
key := firstNonEmpty(strings.TrimSpace(attachment.ID), strings.TrimSpace(attachment.Path))
393+
if _, ok := seen[key]; ok {
394+
continue
395+
}
396+
seen[key] = struct{}{}
397+
result = append(result, attachment)
398+
}
399+
return result
400+
}
401+
402+
func mergeCodexDeepMessage(
403+
target *HistoryItem,
404+
source HistoryItem,
405+
targetOrigin codexDeepMessageOrigin,
406+
sourceOrigin codexDeepMessageOrigin,
407+
) {
408+
if target == nil {
409+
return
410+
}
411+
if sourceOrigin == codexDeepMessageOriginEvent {
412+
target.Text = source.Text
413+
target.Attachments = mergeHistoryAttachments(source.Attachments, target.Attachments)
414+
} else {
415+
target.Attachments = mergeHistoryAttachments(target.Attachments, source.Attachments)
416+
}
417+
if sourceOrigin == codexDeepMessageOriginResponse || targetOrigin != codexDeepMessageOriginResponse {
418+
if source.SourceTurnID != nil {
419+
target.SourceTurnID = source.SourceTurnID
420+
}
421+
if source.SourceItemID != nil {
422+
target.SourceItemID = source.SourceItemID
423+
}
424+
if source.Payload != nil {
425+
target.Payload = source.Payload
426+
}
427+
}
428+
target.Done = target.Done || source.Done
429+
if target.Timestamp == nil || source.Timestamp != nil && source.Timestamp.Before(*target.Timestamp) {
430+
target.Timestamp = source.Timestamp
431+
}
432+
if target.ObservedAt == nil || source.ObservedAt != nil && source.ObservedAt.After(*target.ObservedAt) {
433+
target.ObservedAt = source.ObservedAt
434+
}
435+
}
436+
437+
func codexProposedPlanText(text string) (string, bool) {
438+
trimmed := strings.TrimSpace(strings.ReplaceAll(text, "\r\n", "\n"))
439+
const openTag = "<proposed_plan>"
440+
const closeTag = "</proposed_plan>"
441+
if !strings.HasPrefix(trimmed, openTag) || !strings.HasSuffix(trimmed, closeTag) {
442+
return "", false
443+
}
444+
return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(trimmed, openTag), closeTag)), true
445+
}
446+
447+
func codexDeepItemsShareTurnOrTime(left, right HistoryItem) bool {
448+
if left.SourceTurnID != nil && right.SourceTurnID != nil &&
449+
strings.TrimSpace(*left.SourceTurnID) != "" && *left.SourceTurnID == *right.SourceTurnID {
450+
return true
451+
}
452+
leftTime := codexDeepMessageTime(left)
453+
rightTime := codexDeepMessageTime(right)
454+
if leftTime == nil || rightTime == nil {
455+
return false
456+
}
457+
delta := leftTime.Sub(*rightTime)
458+
if delta < 0 {
459+
delta = -delta
460+
}
461+
return delta <= codexDeepMessageDedupeWindow
462+
}
463+
464+
func dedupeCodexDeepPlanMessages(items []HistoryItem) []HistoryItem {
465+
if len(items) == 0 {
466+
return items
467+
}
468+
result := make([]HistoryItem, 0, len(items))
469+
for index, item := range items {
470+
planText, proposed := codexProposedPlanText(item.Text)
471+
if item.Kind == "assistant" && proposed {
472+
duplicate := false
473+
for candidateIndex, candidate := range items {
474+
if candidateIndex == index || candidate.Tool == nil || candidate.Tool.Kind != "plan" {
475+
continue
476+
}
477+
if normalizedCodexDeepMessageText(candidate.Tool.Output) == normalizedCodexDeepMessageText(planText) &&
478+
codexDeepItemsShareTurnOrTime(candidate, item) {
479+
duplicate = true
480+
break
481+
}
482+
}
483+
if duplicate {
484+
continue
485+
}
486+
}
487+
result = append(result, item)
488+
}
489+
return result
490+
}
491+
278492
func latestHistoryItemTimestamp(items []HistoryItem) *time.Time {
279493
var latest *time.Time
280494
for _, item := range items {
@@ -635,13 +849,17 @@ func (m *Manager) applyCodexResponseItem(
635849
payload map[string]any,
636850
ts time.Time,
637851
appendItem func(item HistoryItem) int,
852+
appendMessage func(item HistoryItem, origin codexDeepMessageOrigin) int,
638853
pendingTools map[string]int,
639854
pendingUserInputs map[string]int,
640855
) {
641856
responseType := strings.TrimSpace(stringValue(payload["type"]))
642857
switch responseType {
643858
case "message":
644-
role := strings.TrimSpace(stringValue(payload["role"]))
859+
role := strings.ToLower(strings.TrimSpace(stringValue(payload["role"])))
860+
if role == "developer" || role == "system" {
861+
return
862+
}
645863
content := decodeRawArray(payload["content"])
646864
textParts := make([]string, 0, len(content))
647865
for _, block := range content {
@@ -653,35 +871,34 @@ func (m *Manager) applyCodexResponseItem(
653871
}
654872
}
655873
}
656-
text := strings.TrimSpace(strings.Join(textParts, "\n"))
874+
text, embeddedAttachments := m.normalizeCodexResponseMessage(strings.Join(textParts, "\n"))
657875
if text == "" {
658876
return
659877
}
660878
item := HistoryItem{
661-
ID: utils.NewID(),
662-
ItemType: "message",
663-
Text: text,
664-
Timestamp: ptr(ts),
665-
ObservedAt: ptr(ts),
666-
Payload: cloneMap(payload),
667-
Done: true,
879+
ID: utils.NewID(),
880+
SourceTurnID: nilIfEmptyHistory(codexResponseItemTurnID(payload)),
881+
SourceItemID: nilIfEmptyHistory(stringValue(payload["id"])),
882+
ItemType: "message",
883+
Text: text,
884+
Timestamp: ptr(ts),
885+
ObservedAt: ptr(ts),
886+
Attachments: embeddedAttachments,
887+
Payload: cloneMap(payload),
888+
Done: true,
668889
}
669890
switch role {
670891
case "assistant":
671892
item.Kind = "assistant"
672893
item.ItemType = "agent_message"
673-
case "developer", "system":
674-
item.Kind = "system"
675-
item.ItemType = "system_message"
676-
item.Level = "info"
677894
default:
678-
if isHiddenCodexPrompt(text) {
895+
if isHiddenCodexPrompt(text) || isCodexInjectedHostContext(text) {
679896
return
680897
}
681898
item.Kind = "user"
682899
item.ItemType = "user_message"
683900
}
684-
appendItem(item)
901+
appendMessage(item, codexDeepMessageOriginResponse)
685902
case "reasoning":
686903
text := codexReasoningSummary(payload)
687904
if text == "" {
@@ -986,6 +1203,41 @@ func codexReasoningSummary(payload map[string]any) string {
9861203
return strings.TrimSpace(strings.Join(parts, "\n"))
9871204
}
9881205

1206+
func codexResponseItemTurnID(payload map[string]any) string {
1207+
metadata := decodeRawObject(payload["internal_chat_message_metadata_passthrough"])
1208+
return strings.TrimSpace(firstNonEmpty(
1209+
stringValue(metadata["turn_id"]),
1210+
stringValue(payload["turn_id"]),
1211+
))
1212+
}
1213+
1214+
func isCodexInjectedHostContext(text string) bool {
1215+
trimmed := strings.TrimSpace(text)
1216+
if strings.HasPrefix(trimmed, "# AGENTS.md instructions for ") &&
1217+
strings.Contains(trimmed, "<INSTRUCTIONS>") &&
1218+
strings.Contains(trimmed, "</INSTRUCTIONS>") {
1219+
return true
1220+
}
1221+
return strings.HasPrefix(trimmed, "<environment_context>") &&
1222+
strings.HasSuffix(trimmed, "</environment_context>")
1223+
}
1224+
1225+
func (m *Manager) normalizeCodexResponseMessage(text string) (string, []HistoryAttachment) {
1226+
attachments := make([]HistoryAttachment, 0)
1227+
for _, match := range codexEmbeddedImageEnvelopePattern.FindAllStringSubmatch(text, -1) {
1228+
if len(match) < 2 {
1229+
continue
1230+
}
1231+
attachment, err := m.registerExternalAttachment(strings.TrimSpace(match[1]))
1232+
if err != nil {
1233+
continue
1234+
}
1235+
attachments = mergeHistoryAttachments(attachments, []HistoryAttachment{attachment})
1236+
}
1237+
text = codexEmbeddedImageEnvelopePattern.ReplaceAllString(text, "")
1238+
return strings.TrimSpace(text), attachments
1239+
}
1240+
9891241
func (m *Manager) codexHistoryAttachments(payload map[string]any) []HistoryAttachment {
9901242
sources := append([]string{}, extractCodexAttachmentSources(payload["local_images"])...)
9911243
sources = append(sources, extractCodexAttachmentSources(payload["images"])...)

0 commit comments

Comments
 (0)