pjpjq commited on
Commit
dca71c4
·
1 Parent(s): 90b818d

fix(proxy): 同步当前 Notion 推理协议

Browse files
internal/proxy/notion.go CHANGED
@@ -1275,6 +1275,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
1275
  session := opt.Session
1276
 
1277
  var reqBody NotionInferenceRequest
 
1278
 
1279
  if session != nil && session.TurnCount > 0 {
1280
  // ── Subsequent turn: partial transcript ──
@@ -1293,31 +1294,30 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
1293
  IsPartialTranscript: true,
1294
  GenerateTitle: false,
1295
  SaveAllThreadOperations: true,
1296
- SetUnreadState: false,
1297
  ThreadType: "workflow",
1298
- AsPatchResponse: false,
1299
- DebugOverrides: DebugOverrides{
1300
- Model: notionModel,
1301
- EmitAgentSearchExtractedResults: true,
1302
- },
1303
  }
1304
  log.Printf("[session] subsequent turn %d on thread %s (updated-configs=%d)",
1305
  session.TurnCount+1, session.ThreadID, len(session.UpdatedConfigIDs))
1306
  } else {
1307
  // ── First turn (or legacy single-turn): full transcript ──
1308
- var configID, contextID, now string
 
 
 
1309
  if session != nil {
1310
  // Pre-created session from HandleAnthropicMessages
1311
  configID = session.ConfigID
1312
  contextID = session.ContextID
 
1313
  now = session.OriginalDatetime
1314
  } else {
1315
  // Legacy single-turn fallback (e.g. OpenAI-compatible handler)
1316
  configID = generateUUIDv4()
1317
  contextID = generateUUIDv4()
 
1318
  now = time.Now().Format(time.RFC3339Nano)
1319
  }
1320
- transcript := buildFullTranscript(acc, messages, notionModel, disableBuiltinTools, enableWebSearch, opt.EnableWorkspaceSearch, opt.UseReadOnlyMode, attachments, configID, contextID, now)
1321
 
1322
  // When attachments are present, reuse the upload thread instead of creating a new one.
1323
  createThread := true
@@ -1341,13 +1341,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
1341
  IsPartialTranscript: false,
1342
  GenerateTitle: true,
1343
  SaveAllThreadOperations: true,
1344
- SetUnreadState: false,
1345
  ThreadType: "workflow",
1346
- AsPatchResponse: false,
1347
- DebugOverrides: DebugOverrides{
1348
- Model: notionModel,
1349
- EmitAgentSearchExtractedResults: true,
1350
- },
1351
  }
1352
 
1353
  if createThread {
@@ -1360,6 +1354,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
1360
 
1361
  log.Printf("[session] first turn, thread %s (session=%v)", threadID, session != nil)
1362
  }
 
1363
 
1364
  bodyBytes, err := json.Marshal(reqBody)
1365
  if err != nil {
@@ -1405,6 +1400,22 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
1405
  return parseNDJSONStream(reader, requestID, cb, opt.NativeToolUses, opt.ThinkingBlocks, opt.ThinkingCallback, opt.KnownCitationURLs, opt.KnownCitationDocs, opt.KnownToolCallURLs)
1406
  }
1407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1408
  // buildConfigValue constructs the Notion config value map used in transcript config entries.
1409
  // enableWorkspaceSearch: nil = use AppConfig default, non-nil = per-request override
1410
  // useReadOnlyMode: when true, sets Notion's ASK-mode flag — model answers
@@ -1423,21 +1434,63 @@ func buildConfigValue(notionModel string, disableBuiltinTools bool, enableWebSea
1423
  agentEnabled := !effectiveDisable || wsSearch
1424
 
1425
  configValue := map[string]interface{}{
1426
- "type": "workflow",
1427
- "model": notionModel,
1428
- "modelFromUser": !isSubsequentTurn,
1429
- "enableAgentAutomations": agentEnabled,
1430
- "enableAgentIntegrations": agentEnabled,
1431
- "enableCustomAgents": !effectiveDisable,
1432
- "enableAgentDiffs": !effectiveDisable,
1433
- "enableCsvAttachmentSupport": true,
1434
- "enableScriptAgent": !effectiveDisable,
1435
- "enableCreateAndRunThread": true,
1436
- "useWebSearch": enableWebSearch,
1437
- "useReadOnlyMode": useReadOnlyMode,
1438
- "writerMode": false,
1439
- "isCustomAgent": false,
1440
- "isCustomAgentBuilder": false,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1441
  }
1442
 
1443
  // searchScopes controls what the built-in search tool can access
@@ -1450,14 +1503,17 @@ func buildConfigValue(notionModel string, disableBuiltinTools bool, enableWebSea
1450
  }
1451
 
1452
  if isSubsequentTurn {
1453
- configValue["isThreadStartedByAdmin"] = true
 
 
 
1454
  }
1455
 
1456
  return configValue
1457
  }
1458
 
1459
  // buildContextValue constructs the Notion context value map used in transcript context entries.
1460
- func buildContextValue(acc *Account, datetime string) map[string]interface{} {
1461
  return map[string]interface{}{
1462
  "timezone": acc.Timezone,
1463
  "userName": acc.UserName,
@@ -1468,15 +1524,17 @@ func buildContextValue(acc *Account, datetime string) map[string]interface{} {
1468
  "spaceViewId": acc.SpaceViewID,
1469
  "currentDatetime": datetime,
1470
  "surface": "ai_module",
 
 
1471
  }
1472
  }
1473
 
1474
  // buildFullTranscript builds a complete transcript for the first turn of a conversation.
1475
  // Uses ResearcherTranscriptMsg (with id field) to match Notion's real client format.
1476
- func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, attachments []UploadedAttachment, configID, contextID, now string) []interface{} {
1477
  hasAttachments := len(attachments) > 0
1478
  configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, hasAttachments, false)
1479
- contextValue := buildContextValue(acc, now)
1480
 
1481
  if hasAttachments {
1482
  contextValue["surface"] = "workflows"
@@ -1555,7 +1613,11 @@ func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel strin
1555
  // It includes: config + context (reused IDs) + N updated-config placeholders + new user message.
1556
  func buildPartialTranscript(acc *Account, newUserContent string, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, session *Session) []interface{} {
1557
  configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, false, true)
1558
- contextValue := buildContextValue(acc, session.OriginalDatetime)
 
 
 
 
1559
 
1560
  transcript := []interface{}{
1561
  ResearcherTranscriptMsg{
@@ -1568,6 +1630,11 @@ func buildPartialTranscript(acc *Account, newUserContent string, notionModel str
1568
  Type: "context",
1569
  Value: contextValue,
1570
  },
 
 
 
 
 
1571
  }
1572
 
1573
  // Add updated-config placeholders for each previous turn
@@ -1584,12 +1651,19 @@ func buildPartialTranscript(acc *Account, newUserContent string, notionModel str
1584
  Type: "user",
1585
  Value: [][]string{{newUserContent}},
1586
  UserID: acc.UserID,
1587
- CreatedAt: time.Now().Format(time.RFC3339Nano),
1588
  })
1589
 
1590
  return transcript
1591
  }
1592
 
 
 
 
 
 
 
 
1593
  func setNotionHeaders(req *http.Request, acc *Account) {
1594
  // Content negotiation
1595
  req.Header.Set("Content-Type", "application/json")
@@ -2309,6 +2383,7 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
2309
  patchValueTypes := make(map[string]string)
2310
  // Counter: "/s/N" → how many value entries added so far
2311
  patchValueCounts := make(map[string]int)
 
2312
  // Accumulated thinking content from patch operations
2313
  var patchThinkingContent string
2314
  var patchThinkingSignature string
@@ -2392,6 +2467,37 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
2392
  sentClean = cleaned
2393
  }
2394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2395
  for scanner.Scan() {
2396
  line := strings.TrimSpace(scanner.Text())
2397
  if line == "" {
@@ -2635,23 +2741,42 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
2635
  cb("", true, &totalUsage)
2636
  }
2637
 
 
 
 
 
 
 
 
 
 
 
2638
  case "patch":
2639
  var patch PatchEvent
2640
  if err := json.Unmarshal([]byte(line), &patch); err != nil {
2641
  continue
2642
  }
2643
  for _, op := range patch.V {
 
 
 
 
 
 
 
 
 
 
 
 
 
2644
  // Track value entry types when new entries are added
2645
  if op.O == "a" && strings.Contains(op.P, "/value/-") {
2646
- var entry struct {
2647
- Type string `json:"type"`
2648
- }
2649
- if json.Unmarshal(op.V, &entry) == nil && entry.Type != "" {
2650
  statePrefix := op.P[:strings.Index(op.P, "/value/")]
2651
  idx := patchValueCounts[statePrefix]
2652
- path := fmt.Sprintf("%s/value/%d", statePrefix, idx)
2653
- patchValueTypes[path] = entry.Type
2654
- patchValueCounts[statePrefix] = idx + 1
2655
  }
2656
  }
2657
 
@@ -2666,12 +2791,14 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
2666
  }
2667
 
2668
  // Handle finishedAt for thinking → flush thinking block
2669
- if op.O == "a" && strings.Contains(op.P, "/finishedAt") && strings.Contains(op.P, "/value/") {
2670
- if thinkingBlocks != nil && patchThinkingContent != "" {
2671
- *thinkingBlocks = append(*thinkingBlocks, ThinkingBlock{
2672
- Content: patchThinkingContent,
2673
- Signature: patchThinkingSignature,
2674
- })
 
 
2675
  patchThinkingContent = ""
2676
  patchThinkingSignature = ""
2677
  }
 
1275
  session := opt.Session
1276
 
1277
  var reqBody NotionInferenceRequest
1278
+ createdSource := "ai_module"
1279
 
1280
  if session != nil && session.TurnCount > 0 {
1281
  // ── Subsequent turn: partial transcript ──
 
1294
  IsPartialTranscript: true,
1295
  GenerateTitle: false,
1296
  SaveAllThreadOperations: true,
 
1297
  ThreadType: "workflow",
 
 
 
 
 
1298
  }
1299
  log.Printf("[session] subsequent turn %d on thread %s (updated-configs=%d)",
1300
  session.TurnCount+1, session.ThreadID, len(session.UpdatedConfigIDs))
1301
  } else {
1302
  // ── First turn (or legacy single-turn): full transcript ──
1303
+ if len(attachments) > 0 {
1304
+ createdSource = "workflows"
1305
+ }
1306
+ var configID, contextID, contextPageID, now string
1307
  if session != nil {
1308
  // Pre-created session from HandleAnthropicMessages
1309
  configID = session.ConfigID
1310
  contextID = session.ContextID
1311
+ contextPageID = ensureSessionContextPageID(session)
1312
  now = session.OriginalDatetime
1313
  } else {
1314
  // Legacy single-turn fallback (e.g. OpenAI-compatible handler)
1315
  configID = generateUUIDv4()
1316
  contextID = generateUUIDv4()
1317
+ contextPageID = generateUUIDv4()
1318
  now = time.Now().Format(time.RFC3339Nano)
1319
  }
1320
+ transcript := buildFullTranscript(acc, messages, notionModel, disableBuiltinTools, enableWebSearch, opt.EnableWorkspaceSearch, opt.UseReadOnlyMode, attachments, configID, contextID, contextPageID, now)
1321
 
1322
  // When attachments are present, reuse the upload thread instead of creating a new one.
1323
  createThread := true
 
1341
  IsPartialTranscript: false,
1342
  GenerateTitle: true,
1343
  SaveAllThreadOperations: true,
 
1344
  ThreadType: "workflow",
 
 
 
 
 
1345
  }
1346
 
1347
  if createThread {
 
1354
 
1355
  log.Printf("[session] first turn, thread %s (session=%v)", threadID, session != nil)
1356
  }
1357
+ applyWorkflowRequestProtocol(&reqBody, createdSource)
1358
 
1359
  bodyBytes, err := json.Marshal(reqBody)
1360
  if err != nil {
 
1400
  return parseNDJSONStream(reader, requestID, cb, opt.NativeToolUses, opt.ThinkingBlocks, opt.ThinkingCallback, opt.KnownCitationURLs, opt.KnownCitationDocs, opt.KnownToolCallURLs)
1401
  }
1402
 
1403
+ func applyWorkflowRequestProtocol(reqBody *NotionInferenceRequest, createdSource string) {
1404
+ reqBody.SetUnreadState = true
1405
+ reqBody.CreatedSource = createdSource
1406
+ reqBody.AsPatchResponse = true
1407
+ reqBody.PatchResponseVersion = 2
1408
+ reqBody.IsUserInAnySalesAssistedSpace = boolPtr(false)
1409
+ reqBody.IsSpaceSalesAssisted = boolPtr(false)
1410
+ reqBody.SupportsCustomAgentNudgeTranscriptStep = boolPtr(true)
1411
+ reqBody.DebugOverrides = DebugOverrides{
1412
+ EmitAgentSearchExtractedResults: true,
1413
+ CachedInferences: &struct{}{},
1414
+ AnnotationInferences: &struct{}{},
1415
+ EmitInferences: boolPtr(false),
1416
+ }
1417
+ }
1418
+
1419
  // buildConfigValue constructs the Notion config value map used in transcript config entries.
1420
  // enableWorkspaceSearch: nil = use AppConfig default, non-nil = per-request override
1421
  // useReadOnlyMode: when true, sets Notion's ASK-mode flag — model answers
 
1434
  agentEnabled := !effectiveDisable || wsSearch
1435
 
1436
  configValue := map[string]interface{}{
1437
+ "type": "workflow",
1438
+ "enableAgentAutomations": agentEnabled,
1439
+ "enableAgentIntegrations": agentEnabled,
1440
+ "enableCustomAgents": !effectiveDisable,
1441
+ "enableExperimentalIntegrations": false,
1442
+ "enableScriptAgent": !effectiveDisable,
1443
+ "enableScriptAgentAdvanced": false,
1444
+ "enableScriptAgentSearchConnectorsInCustomAgent": false,
1445
+ "enableScriptAgentGoogleDriveInCustomAgent": false,
1446
+ "enableScriptAgentGoogleDriveOAuthInCustomAgent": false,
1447
+ "enableScriptAgentSlack": !effectiveDisable,
1448
+ "enableScriptAgentMcpServers": !effectiveDisable,
1449
+ "enableAgentDiffs": !effectiveDisable,
1450
+ "enableCsvAttachmentSupport": true,
1451
+ "showDatabaseAgentsDiscoverability": true,
1452
+ "enableAgentThreadTools": false,
1453
+ "enableCrdtOperations": false,
1454
+ "enableAgentCardCustomization": true,
1455
+ "enableSystemPromptAsPage": false,
1456
+ "enableUserSessionContext": false,
1457
+ "enableLargeToolResultComputerOffload": false,
1458
+ "enableScriptAgentGtm": false,
1459
+ "enablePitCrewTableViewTool": false,
1460
+ "enableComputer": !effectiveDisable,
1461
+ "enableCustomAgentCreateGuidanceV2": true,
1462
+ "enableSoftwareFactoryPage": false,
1463
+ "enableAgentGenerateImage": !effectiveDisable,
1464
+ "enableQueryCalendar": false,
1465
+ "enableQueryMail": false,
1466
+ "enableMailExplicitToolCalls": true,
1467
+ "enableMailNotificationPreferences": false,
1468
+ "enableMailAgentMultiProviderSupport": true,
1469
+ "enableNotionMailDeprecated": false,
1470
+ "enableWebResearch": false,
1471
+ "useRulePrioritization": true,
1472
+ "useWebSearch": enableWebSearch,
1473
+ "isHipaa": false,
1474
+ "internetAccess": false,
1475
+ "manageWorkers": false,
1476
+ "useReadOnlyMode": useReadOnlyMode,
1477
+ "writerMode": false,
1478
+ "model": notionModel,
1479
+ "modelFromUser": true,
1480
+ "isCustomAgent": false,
1481
+ "isCustomAgentBuilder": false,
1482
+ "isCustomAgentCreate": false,
1483
+ "isAgentResearchRequest": false,
1484
+ "useCustomAgentDraft": false,
1485
+ "enableMarkdownVNext": false,
1486
+ "enableAgentSkillsV2": false,
1487
+ "updatePageStaleViewGuardEnabled": false,
1488
+ "enableUpdatePageOrderUpdates": true,
1489
+ "enableAgentSupportPropertyReorder": true,
1490
+ "enableAgentAskSurvey": true,
1491
+ "databaseAgentConfigMode": false,
1492
+ "isOnboardingAgent": false,
1493
+ "isMobile": false,
1494
  }
1495
 
1496
  // searchScopes controls what the built-in search tool can access
 
1503
  }
1504
 
1505
  if isSubsequentTurn {
1506
+ configValue["useContextualCoreDocsAutoLoad"] = false
1507
+ configValue["useDocPreviewsForCoreAutoLoad"] = true
1508
+ } else {
1509
+ configValue["availableConnectors"] = []interface{}{}
1510
  }
1511
 
1512
  return configValue
1513
  }
1514
 
1515
  // buildContextValue constructs the Notion context value map used in transcript context entries.
1516
+ func buildContextValue(acc *Account, datetime, contextPageID string) map[string]interface{} {
1517
  return map[string]interface{}{
1518
  "timezone": acc.Timezone,
1519
  "userName": acc.UserName,
 
1524
  "spaceViewId": acc.SpaceViewID,
1525
  "currentDatetime": datetime,
1526
  "surface": "ai_module",
1527
+ "agentAccessory": "paprika",
1528
+ "context_page_id": contextPageID,
1529
  }
1530
  }
1531
 
1532
  // buildFullTranscript builds a complete transcript for the first turn of a conversation.
1533
  // Uses ResearcherTranscriptMsg (with id field) to match Notion's real client format.
1534
+ func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, attachments []UploadedAttachment, configID, contextID, contextPageID, now string) []interface{} {
1535
  hasAttachments := len(attachments) > 0
1536
  configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, hasAttachments, false)
1537
+ contextValue := buildContextValue(acc, now, contextPageID)
1538
 
1539
  if hasAttachments {
1540
  contextValue["surface"] = "workflows"
 
1613
  // It includes: config + context (reused IDs) + N updated-config placeholders + new user message.
1614
  func buildPartialTranscript(acc *Account, newUserContent string, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, session *Session) []interface{} {
1615
  configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, false, true)
1616
+ contextPageID := ensureSessionContextPageID(session)
1617
+ contextValue := buildContextValue(acc, session.OriginalDatetime, contextPageID)
1618
+ currentDatetime := time.Now().Format(time.RFC3339Nano)
1619
+ currentContextValue := buildContextValue(acc, currentDatetime, contextPageID)
1620
+ currentContextValue["surface"] = "full_page_chat"
1621
 
1622
  transcript := []interface{}{
1623
  ResearcherTranscriptMsg{
 
1630
  Type: "context",
1631
  Value: contextValue,
1632
  },
1633
+ ResearcherTranscriptMsg{
1634
+ ID: generateUUIDv4(),
1635
+ Type: "context",
1636
+ Value: currentContextValue,
1637
+ },
1638
  }
1639
 
1640
  // Add updated-config placeholders for each previous turn
 
1651
  Type: "user",
1652
  Value: [][]string{{newUserContent}},
1653
  UserID: acc.UserID,
1654
+ CreatedAt: currentDatetime,
1655
  })
1656
 
1657
  return transcript
1658
  }
1659
 
1660
+ func ensureSessionContextPageID(session *Session) string {
1661
+ if session.ContextPageID == "" {
1662
+ session.ContextPageID = generateUUIDv4()
1663
+ }
1664
+ return session.ContextPageID
1665
+ }
1666
+
1667
  func setNotionHeaders(req *http.Request, acc *Account) {
1668
  // Content negotiation
1669
  req.Header.Set("Content-Type", "application/json")
 
2383
  patchValueTypes := make(map[string]string)
2384
  // Counter: "/s/N" → how many value entries added so far
2385
  patchValueCounts := make(map[string]int)
2386
+ patchNextStepIndex := 0
2387
  // Accumulated thinking content from patch operations
2388
  var patchThinkingContent string
2389
  var patchThinkingSignature string
 
2467
  sentClean = cleaned
2468
  }
2469
 
2470
+ handlePatchValueEntry := func(statePrefix string, index int, entry AgentValueEntry) {
2471
+ patchValueTypes[fmt.Sprintf("%s/value/%d", statePrefix, index)] = entry.Type
2472
+ if patchValueCounts[statePrefix] <= index {
2473
+ patchValueCounts[statePrefix] = index + 1
2474
+ }
2475
+
2476
+ switch entry.Type {
2477
+ case "thinking":
2478
+ prev := patchThinkingContent
2479
+ patchThinkingContent += entry.Content
2480
+ if entry.Signature != "" {
2481
+ patchThinkingSignature = entry.Signature
2482
+ lastThinkingSignature = entry.Signature
2483
+ }
2484
+ emitThinking(incrementalSuffix(prev, patchThinkingContent))
2485
+ case "text":
2486
+ if entry.Content != "" {
2487
+ rawText += entry.Content
2488
+ emitDelta()
2489
+ }
2490
+ case "tool_use":
2491
+ // Skip — tool state must not pollute text output.
2492
+ if entry.Name != "" && entry.ID != "" && !seenNativeToolUseIDs[entry.ID] {
2493
+ seenNativeToolUseIDs[entry.ID] = true
2494
+ if nativeToolUses != nil {
2495
+ *nativeToolUses = append(*nativeToolUses, entry)
2496
+ }
2497
+ }
2498
+ }
2499
+ }
2500
+
2501
  for scanner.Scan() {
2502
  line := strings.TrimSpace(scanner.Text())
2503
  if line == "" {
 
2741
  cb("", true, &totalUsage)
2742
  }
2743
 
2744
+ case "patch-start":
2745
+ var patchStart struct {
2746
+ Data struct {
2747
+ Steps []json.RawMessage `json:"s"`
2748
+ } `json:"data"`
2749
+ }
2750
+ if json.Unmarshal([]byte(line), &patchStart) == nil {
2751
+ patchNextStepIndex = len(patchStart.Data.Steps)
2752
+ }
2753
+
2754
  case "patch":
2755
  var patch PatchEvent
2756
  if err := json.Unmarshal([]byte(line), &patch); err != nil {
2757
  continue
2758
  }
2759
  for _, op := range patch.V {
2760
+ if op.O == "a" && op.P == "/s/-" {
2761
+ stepIndex := patchNextStepIndex
2762
+ patchNextStepIndex++
2763
+
2764
+ var step AgentInferenceEvent
2765
+ if json.Unmarshal(op.V, &step) == nil && step.Type == "agent-inference" {
2766
+ statePrefix := fmt.Sprintf("/s/%d", stepIndex)
2767
+ for idx, entry := range step.Value {
2768
+ handlePatchValueEntry(statePrefix, idx, entry)
2769
+ }
2770
+ }
2771
+ }
2772
+
2773
  // Track value entry types when new entries are added
2774
  if op.O == "a" && strings.Contains(op.P, "/value/-") {
2775
+ var entry AgentValueEntry
2776
+ if json.Unmarshal(op.V, &entry) == nil {
 
 
2777
  statePrefix := op.P[:strings.Index(op.P, "/value/")]
2778
  idx := patchValueCounts[statePrefix]
2779
+ handlePatchValueEntry(statePrefix, idx, entry)
 
 
2780
  }
2781
  }
2782
 
 
2791
  }
2792
 
2793
  // Handle finishedAt for thinking → flush thinking block
2794
+ if op.O == "a" && strings.HasSuffix(op.P, "/finishedAt") {
2795
+ if patchThinkingContent != "" {
2796
+ if thinkingBlocks != nil {
2797
+ *thinkingBlocks = append(*thinkingBlocks, ThinkingBlock{
2798
+ Content: patchThinkingContent,
2799
+ Signature: patchThinkingSignature,
2800
+ })
2801
+ }
2802
  patchThinkingContent = ""
2803
  patchThinkingSignature = ""
2804
  }
internal/proxy/session.go CHANGED
@@ -23,6 +23,9 @@ type Session struct {
23
  ConfigID string
24
  ContextID string
25
 
 
 
 
26
  // Each completed turn produces one updated-config placeholder ID
27
  UpdatedConfigIDs []string
28
 
 
23
  ConfigID string
24
  ContextID string
25
 
26
+ // ContextPageID is generated independently from the transcript IDs and reused across turns.
27
+ ContextPageID string
28
+
29
  // Each completed turn produces one updated-config placeholder ID
30
  UpdatedConfigIDs []string
31
 
internal/proxy/types.go CHANGED
@@ -150,19 +150,24 @@ type UsageInfo struct {
150
  // ========== Notion API Types ==========
151
 
152
  type NotionInferenceRequest struct {
153
- TraceID string `json:"traceId"`
154
- SpaceID string `json:"spaceId"`
155
- ThreadID string `json:"threadId,omitempty"`
156
- Transcript []interface{} `json:"transcript"`
157
- CreateThread bool `json:"createThread"`
158
- GenerateTitle bool `json:"generateTitle"`
159
- SaveAllThreadOperations bool `json:"saveAllThreadOperations"`
160
- SetUnreadState bool `json:"setUnreadState"`
161
- ThreadType string `json:"threadType"`
162
- AsPatchResponse bool `json:"asPatchResponse"`
163
- IsPartialTranscript bool `json:"isPartialTranscript"`
164
- ThreadParentPointer *ThreadParentPointer `json:"threadParentPointer,omitempty"`
165
- DebugOverrides DebugOverrides `json:"debugOverrides"`
 
 
 
 
 
166
  }
167
 
168
  // ThreadParentPointer identifies the parent of a thread (used only on first turn)
@@ -194,8 +199,10 @@ type ResearcherTranscriptMsg struct {
194
  }
195
 
196
  type DebugOverrides struct {
197
- Model string `json:"model,omitempty"`
198
- EmitAgentSearchExtractedResults bool `json:"emitAgentSearchExtractedResults,omitempty"`
 
 
199
  }
200
 
201
  type NDJSONEvent struct {
 
150
  // ========== Notion API Types ==========
151
 
152
  type NotionInferenceRequest struct {
153
+ TraceID string `json:"traceId"`
154
+ SpaceID string `json:"spaceId"`
155
+ ThreadID string `json:"threadId,omitempty"`
156
+ Transcript []interface{} `json:"transcript"`
157
+ CreateThread bool `json:"createThread"`
158
+ GenerateTitle bool `json:"generateTitle"`
159
+ SaveAllThreadOperations bool `json:"saveAllThreadOperations"`
160
+ SetUnreadState bool `json:"setUnreadState"`
161
+ ThreadType string `json:"threadType"`
162
+ CreatedSource string `json:"createdSource,omitempty"`
163
+ AsPatchResponse bool `json:"asPatchResponse"`
164
+ PatchResponseVersion int `json:"patchResponseVersion,omitempty"`
165
+ IsPartialTranscript bool `json:"isPartialTranscript"`
166
+ IsUserInAnySalesAssistedSpace *bool `json:"isUserInAnySalesAssistedSpace,omitempty"`
167
+ IsSpaceSalesAssisted *bool `json:"isSpaceSalesAssisted,omitempty"`
168
+ SupportsCustomAgentNudgeTranscriptStep *bool `json:"supportsCustomAgentNudgeTranscriptStep,omitempty"`
169
+ ThreadParentPointer *ThreadParentPointer `json:"threadParentPointer,omitempty"`
170
+ DebugOverrides DebugOverrides `json:"debugOverrides"`
171
  }
172
 
173
  // ThreadParentPointer identifies the parent of a thread (used only on first turn)
 
199
  }
200
 
201
  type DebugOverrides struct {
202
+ EmitAgentSearchExtractedResults bool `json:"emitAgentSearchExtractedResults,omitempty"`
203
+ CachedInferences *struct{} `json:"cachedInferences,omitempty"`
204
+ AnnotationInferences *struct{} `json:"annotationInferences,omitempty"`
205
+ EmitInferences *bool `json:"emitInferences,omitempty"`
206
  }
207
 
208
  type NDJSONEvent struct {