claude-smart 0.2.49 → 0.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -0,0 +1,270 @@
1
+ """Write-plan dataclasses for the durable-learning compute/persist split (gate b).
2
+
3
+ Compute resolves these in-memory plans issuing **no** learning DB write; persist
4
+ applies them inside one short fenced ``commit_scope``. This module starts with the
5
+ extractor stride-bookmark advance and grows additional write-plan dataclasses in the
6
+ later gate-(b) tasks (profile/playbook/reflection/generation/deferred plans).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ if TYPE_CHECKING:
15
+ from reflexio.models.api_schema.domain.entities import (
16
+ UserPlaybook,
17
+ UserProfile,
18
+ )
19
+ from reflexio.models.api_schema.service_schemas import Interaction
20
+ from reflexio.server.llm.token_accounting import RunTokenTotals
21
+ from reflexio.server.services.base_generation_service import (
22
+ BaseGenerationService,
23
+ PreparedGenerationRun,
24
+ )
25
+ from reflexio.server.services.reflection.reflection_service_utils import (
26
+ ReflectionDecision,
27
+ ReflectionResult,
28
+ ReflectionServiceRequest,
29
+ )
30
+ from reflexio.server.services.reflection.service import ReflectionService
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ExtractorBookmarkAdvance:
35
+ """The extractor stride-bookmark advance, deferred out of the extractor (F1).
36
+
37
+ The extractor no longer self-advances its bookmark inside ``run()`` (a DB
38
+ write). It emits this on the ``ExtractionOutcome`` instead, so the advance is
39
+ applied later — inside the persist fence on the durable path, or right after
40
+ result-processing on the synchronous ``.run()`` path — keeping the bookmark
41
+ advance atomic with the row writes it corresponds to.
42
+ """
43
+
44
+ extractor_name: str
45
+ processed_interactions: list[Interaction]
46
+ # str | None (not str): the playbook extractor runs org-level with
47
+ # user_id=None (PlaybookGenerationServiceConfig.user_id is Optional), which
48
+ # maps to an unscoped bookmark key. update_extractor_bookmark accepts None.
49
+ user_id: str | None
50
+
51
+
52
+ @dataclass
53
+ class ProfileWritePlan:
54
+ """Resolved profile write-plan for the compute/persist split (gate b).
55
+
56
+ ``ProfileGenerationService._resolve_write_plan`` produces this in compute
57
+ (dedup 2nd-LLM call + existing-row reads + source/status assignment +
58
+ precomputed ``.embedding`` via ``precompute_profile_embeddings``), issuing
59
+ **no** learning DB write. ``_persist_write_plan`` applies it inside the
60
+ fence: ``add_user_profile(..., skip_embedding=True)`` (embeddings already
61
+ set) then ``supersede_profiles_by_ids``.
62
+
63
+ Attributes:
64
+ user_id: Owning user id for the row writes.
65
+ request_id: Generation request id — the lineage key
66
+ ``supersede_profiles_by_ids`` records. Compute drops any supersede
67
+ ids when this is empty (unreconstructable), so persist only ever
68
+ supersedes with a non-empty request_id.
69
+ new_profiles: New profile rows with ``source``/``status`` set and
70
+ ``.embedding`` precomputed in compute.
71
+ superseded_ids: Existing profile ids to soft-supersede on the dedup
72
+ path.
73
+ """
74
+
75
+ user_id: str
76
+ request_id: str
77
+ new_profiles: list[UserProfile]
78
+ superseded_ids: list[str]
79
+
80
+
81
+ @dataclass
82
+ class PlaybookWritePlan:
83
+ """Resolved playbook write-plan for the compute/persist split (gate b).
84
+
85
+ ``PlaybookGenerationService._resolve_write_plan`` produces this in compute
86
+ (``dedupe_and_drop_empty`` + the deduplicator's 2nd-LLM call + existing-row
87
+ reads + source/status assignment + precomputed ``.embedding`` via
88
+ ``precompute_user_playbook_embeddings``), issuing **no** learning DB write.
89
+ ``_persist_write_plan`` applies it inside the fence:
90
+ ``save_user_playbooks(..., skip_embedding=True)`` (which assigns survivor
91
+ ids) then ``_apply_consolidation_lineage`` (which MUST see those ids, so it
92
+ runs AFTER the save).
93
+
94
+ The off-thread schedulers (``_enqueue_user_playbook_optimization`` +
95
+ ``_trigger_playbook_aggregation``) are NOT part of persist — they fire
96
+ post-commit in ``emit_generation_side_effects`` (durable / ``.run()`` path)
97
+ or right after persist in the permanent ``_finalize_extracted_items``
98
+ wrapper (synchronous resume/manual path). ``output_pending_status`` /
99
+ ``skip_aggregation`` are snapshotted here so the scheduler dispatch reads the
100
+ plan rather than the reused service instance.
101
+
102
+ Attributes:
103
+ request_id: Generation request id — the lineage key
104
+ ``_apply_consolidation_lineage`` records on merges/supersedes.
105
+ output_pending_status: Whether the run emits PENDING rows (rerun mode);
106
+ when True the aggregation trigger is suppressed.
107
+ skip_aggregation: Whether aggregation is skipped (extract-only); when
108
+ True the aggregation trigger is suppressed.
109
+ new_playbooks: New playbook rows with ``source``/``status`` set and
110
+ ``.embedding`` precomputed in compute. Survivor ids are assigned by
111
+ ``save_user_playbooks`` in persist, before lineage reads them.
112
+ superseded_ids: ALL archived existing ids (merge sources + leftovers)
113
+ routed through ``_apply_consolidation_lineage``.
114
+ merge_groups: ``(survivor_index_into_new_playbooks, source_existing_ids)``
115
+ per dedup merge group.
116
+ """
117
+
118
+ request_id: str
119
+ output_pending_status: bool
120
+ skip_aggregation: bool
121
+ new_playbooks: list[UserPlaybook]
122
+ superseded_ids: list[int]
123
+ merge_groups: list[tuple[int, list[int]]]
124
+
125
+
126
+ @dataclass
127
+ class ReflectionWritePlan:
128
+ """Resolved reflection write-plan for the compute/persist split (F5, V2).
129
+
130
+ ``ReflectionService.compute`` produces this issuing **no** learning DB write;
131
+ ``persist`` applies the whole cap/validate/CAS apply loop verbatim inside the
132
+ fence, then advances the reflection stride-bookmark; ``emit_side_effects``
133
+ fires the post-commit billing + aggregation triggers. The apply loop stays a
134
+ single monolith (no per-revision ``ResolvedReflectionRevision`` dataclass).
135
+
136
+ Attributes:
137
+ request: The originating reflection request (user_id / request_id /
138
+ agent_version).
139
+ result: The reflection result. Compute seeds it with the gate/citation
140
+ counters resolved so far; persist's apply loop mutates it in place
141
+ (revised/failed/skipped/... counters) as it applies each decision.
142
+ decisions: ``ReflectionOutput.decisions`` to apply — ``[]`` on the four
143
+ early bookmark-advance paths (nothing to apply, only advance).
144
+ profiles_by_id: Cited profile rows resolved in compute, keyed by
145
+ ``profile_id`` (the apply loop's ``_apply_revision`` input).
146
+ playbooks_by_id: Cited playbook rows resolved in compute, keyed by
147
+ ``user_playbook_id``.
148
+ max_revisions_per_pass: Per-pass revision cap applied inside the loop.
149
+ bookmark_interactions: The window the reflection stride-bookmark advances
150
+ over in persist (``[]`` on the empty-window advance path).
151
+ advance_bookmark: ``True`` on all five bookmark-advance paths (the four
152
+ early-advance returns + the normal end). Persist advances the
153
+ reflection bookmark iff this is ``True``.
154
+ record_learnings: ``True`` only on the normal-end path — gates the
155
+ post-commit ``_record_learnings_generated`` billing event in emit.
156
+ replacement_profiles: Replacement ``UserProfile`` rows whose embeddings
157
+ were precomputed in compute (V2), keyed by the **cited** profile_id
158
+ they replace. Persist inserts these with ``skip_embedding=True`` so
159
+ no embedding runs inside the fence. Empty on non-compute (direct)
160
+ ``_replace_*`` calls, which then build+embed the row themselves.
161
+ replacement_playbooks: Same, keyed by the cited ``user_playbook_id``.
162
+ aggregation_successor_ids: Playbook successor ids collected during the
163
+ persist apply loop; their aggregation trigger is dispatched in
164
+ ``emit_side_effects`` (post-commit) so it never fires on a
165
+ rolled-back job.
166
+ """
167
+
168
+ request: ReflectionServiceRequest
169
+ result: ReflectionResult
170
+ decisions: list[ReflectionDecision]
171
+ profiles_by_id: dict[str, UserProfile]
172
+ playbooks_by_id: dict[int, UserPlaybook]
173
+ max_revisions_per_pass: int
174
+ bookmark_interactions: list[Interaction]
175
+ advance_bookmark: bool
176
+ record_learnings: bool
177
+ replacement_profiles: dict[str, UserProfile] = field(default_factory=dict)
178
+ replacement_playbooks: dict[int, UserPlaybook] = field(default_factory=dict)
179
+ aggregation_successor_ids: list[int] = field(default_factory=list)
180
+
181
+
182
+ @dataclass
183
+ class GenerationComputePlan:
184
+ """Resolved compute output of one ``BaseGenerationService`` run (gate b).
185
+
186
+ ``compute_generation`` runs the prepare gate + extractor + dedup/embedding
187
+ resolution (``_resolve_write_plan``) and drives the ``agent_run`` rows to
188
+ their terminal state (``_finalize_extraction_runs`` — agent_run only, §4.3),
189
+ issuing **no** learning DB write. ``persist_generation`` applies
190
+ ``write_plan`` + the extractor bookmark advance inside the fence;
191
+ ``emit_generation_side_effects`` fires the post-commit telemetry + billing.
192
+
193
+ The billing inputs (``extraction_run_ids`` / ``token_totals`` /
194
+ ``generated_count`` / ``prepared``) are **snapshotted at compute time** so
195
+ the fence-crossing emit reads this plan rather than the reused service
196
+ instance's mutable ``_last_*`` accumulators (purity contract, plan §File
197
+ Structure). See ``emit_generation_side_effects`` for the single-use-instance
198
+ invariant that also keeps the money helper's ``self._last_*`` reads safe.
199
+
200
+ Attributes:
201
+ prepared: The prepared generation run (identifier / extractor_name /
202
+ extractor_config), reused by emit for telemetry + billing input.
203
+ generated_count: Learnings produced by this extraction run.
204
+ write_plan: The resolved write-plan (``ProfileWritePlan`` /
205
+ ``PlaybookWritePlan`` in Tasks 6-7, a ``_LegacyItems`` shim marker
206
+ until then) or ``None`` when the extractor produced nothing.
207
+ bookmark_advance: The deferred extractor stride-bookmark advance (F1),
208
+ applied inside the persist scope. ``None`` when the extractor
209
+ produced no output or the service has no stride bookmark.
210
+ generation_start: ``perf_counter`` captured at compute start; emit reads
211
+ it for the ``generation_succeeded`` ``duration_ms`` (parity).
212
+ extraction_run_ids: Snapshot of the run's ``agent_run`` ids.
213
+ token_totals: Snapshot of the run's LLM token totals (billing cost
214
+ facet), or ``None`` when the extractor reported none.
215
+ """
216
+
217
+ prepared: PreparedGenerationRun[Any]
218
+ generated_count: int
219
+ write_plan: Any
220
+ bookmark_advance: ExtractorBookmarkAdvance | None
221
+ generation_start: float
222
+ extraction_run_ids: list[str]
223
+ token_totals: RunTokenTotals | None
224
+
225
+
226
+ @dataclass
227
+ class DeferredLearningPlan:
228
+ """One durable-learning job's resolved compute output (gate b, Task 8).
229
+
230
+ ``GenerationService.compute_deferred_learning`` acquires the same-user guard
231
+ (F4), then runs reflection + profile + playbook compute holding **no**
232
+ ``commit_scope``, and assembles this plan. It carries each present half as a
233
+ ``(held_service_instance, its_compute_plan)`` pair so
234
+ ``persist_deferred_learning`` can apply the fence-critical writes and
235
+ ``emit_deferred_learning_side_effects`` can fire the post-commit telemetry on
236
+ the very instances that produced the plans (the single-use-instance invariant
237
+ that keeps the money helpers' ``self._last_*`` reads valid — see
238
+ ``BaseGenerationService.emit_generation_side_effects``).
239
+
240
+ Attributes:
241
+ request_id: The job's generation request id (lineage key).
242
+ user_id: The user the job learns for; also the per-user F4 lock scope.
243
+ agent_version: Resolved agent version — carried so the post-commit
244
+ ``schedule_tagging`` dispatch in emit has it (the plan is all emit
245
+ receives).
246
+ lock_acquired: ``False`` when the F4 same-user guard denied this job
247
+ (another durable job for the same user is mid-flight). On ``False``
248
+ the three plan halves are ``None`` and NO LLM/compute ran — the
249
+ worker (Task 9) must leave the job reclaimable and must NOT
250
+ ``complete_learning_job`` it.
251
+ reflection: ``(ReflectionService, ReflectionWritePlan)`` when reflection
252
+ resolved something to persist, else ``None`` (gate closed / disabled
253
+ / compute error captured into ``warnings``).
254
+ profile: ``(ProfileGenerationService, GenerationComputePlan)`` when the
255
+ profile extractor produced a plan, else ``None``.
256
+ playbook: ``(PlaybookGenerationService, GenerationComputePlan)`` when the
257
+ playbook extractor produced a plan, else ``None``.
258
+ warnings: Best-effort per-half compute failures (mirrors
259
+ ``GenerationServiceResult.warnings``) — a failed half is dropped from
260
+ the plan, the others still persist.
261
+ """
262
+
263
+ request_id: str
264
+ user_id: str
265
+ agent_version: str
266
+ lock_acquired: bool
267
+ reflection: tuple[ReflectionService, ReflectionWritePlan] | None
268
+ profile: tuple[BaseGenerationService, GenerationComputePlan] | None
269
+ playbook: tuple[BaseGenerationService, GenerationComputePlan] | None
270
+ warnings: list[str] = field(default_factory=list)
@@ -1,8 +1,15 @@
1
- """DurableLearningWorker: claim jobs from the durable queue, process in a fenced
2
- commit_scope so exactly one of N racing workers commits its outputs.
1
+ """DurableLearningWorker: claim jobs from the durable queue, split each into
2
+ compute (no writer transaction) -> persist (short fenced commit_scope) ->
3
+ post-commit side-effects so exactly one of N racing workers commits its outputs.
3
4
 
4
- Design constraints (v1 — do not remove):
5
- - LLM calls are inside the commit_scope (compute/persist separation is deferred).
5
+ Design constraints (gate b — do not remove):
6
+ - LLM extraction + dedup + embeddings run in compute_deferred_learning, OUTSIDE
7
+ the commit_scope. Only the fence-critical writes (profile/playbook rows +
8
+ bookmark advances + complete_learning_job) run inside the scope; billing /
9
+ telemetry / tagging / off-thread schedulers + the per-user lock release run
10
+ AFTER the scope commits (emit_deferred_learning_side_effects). Holding the
11
+ writer transaction across the ~30-120s LLM window is exactly what this split
12
+ removes.
6
13
  - No heartbeat thread (deferred; would deadlock a SQLite scope anyway).
7
14
  - Exactly-once is guaranteed by the fenced complete_learning_job:
8
15
  rowcount == 0 -> lease was stolen -> _SupersededError -> rollback.
@@ -27,8 +34,9 @@ class _SupersededError(Exception):
27
34
  """Raised inside commit_scope when complete_learning_job returns 0.
28
35
 
29
36
  Propagating this exception causes commit_scope to roll back all writes
30
- (profiles, playbooks) made by run_deferred_learning for the superseded
31
- worker — guaranteeing exactly-once side effects.
37
+ (profiles, playbooks, bookmark advances) made by persist_deferred_learning
38
+ for the superseded worker — guaranteeing exactly-once side effects. The
39
+ post-commit emit is skipped, so a superseded job phantom-bills nothing.
32
40
  """
33
41
 
34
42
  def __init__(self, job_id: str) -> None:
@@ -142,28 +150,59 @@ class DurableLearningWorker:
142
150
  )
143
151
  return False
144
152
 
153
+ gen: GenerationService | None = None
145
154
  try:
146
155
  reflexio = get_reflexio(
147
156
  org_id=ctx.org_id, storage_base_dir=ctx.storage_base_dir
148
157
  )
149
158
  gen = GenerationService(llm_client=reflexio.llm_client, request_context=ctx)
150
159
 
151
- with storage.commit_scope():
152
- gen.run_deferred_learning(
153
- user_id=job.user_id,
154
- request_id=job.latest_request_id,
155
- session_id=request.session_id,
156
- source=request.source,
157
- agent_version=request.agent_version,
158
- force_extraction=job.force_extraction,
159
- skip_aggregation=job.skip_aggregation,
160
- sequential=True, # prevent ThreadPoolExecutor deadlock on commit_scope RLock
160
+ # COMPUTE — LLM extraction + dedup + embeddings, NO writer transaction
161
+ # held. Acquires the per-user F4 lock; issues no learning DB write.
162
+ plan = gen.compute_deferred_learning(
163
+ user_id=job.user_id,
164
+ request_id=job.latest_request_id,
165
+ session_id=request.session_id,
166
+ source=request.source,
167
+ agent_version=request.agent_version,
168
+ force_extraction=job.force_extraction,
169
+ skip_aggregation=job.skip_aggregation,
170
+ )
171
+
172
+ # Same-user contention (F4): another same-user durable job holds the
173
+ # per-user lock. Leave THIS job reclaimable (dead=False) — do NOT
174
+ # complete it — so the queue re-claims it once the holder finishes.
175
+ # REFUND the attempt (refund_attempt=True): claim_learning_jobs did
176
+ # attempts += 1 on this claim, but no real work ran, and the ~2s poll
177
+ # would otherwise re-claim (and re-increment) every couple of seconds
178
+ # while the holder is in its ~60s compute — inflating attempts past
179
+ # max_attempts in seconds so the eventual winner dead-letters on its
180
+ # first transient error with zero real retries. The refund nets each
181
+ # contention cycle (claim +1, release -1) to zero. No lock to release
182
+ # (compute never acquired it).
183
+ if not plan.lock_acquired:
184
+ storage.fail_learning_job(
185
+ job_id=job.job_id,
186
+ claim_token=claim_token,
187
+ dead=False,
188
+ refund_attempt=True,
161
189
  )
190
+ return False
191
+
192
+ # PERSIST — short fenced scope: fence-critical writes + bookmark
193
+ # advances only, then the claim-token fence. rows == 0 -> lease
194
+ # stolen -> _SupersededError -> the scope rolls persist back.
195
+ with storage.commit_scope():
196
+ gen.persist_deferred_learning(plan)
162
197
  rows = storage.complete_learning_job(
163
198
  job_id=job.job_id, claim_token=claim_token
164
199
  )
165
200
  if rows == 0:
166
201
  raise _SupersededError(job.job_id)
202
+
203
+ # POST-COMMIT — billing / telemetry / tagging / off-thread schedulers
204
+ # + the per-user lock release, only for the winning worker.
205
+ gen.emit_deferred_learning_side_effects(plan)
167
206
  logger.info(
168
207
  "event=learning_job_done job_id=%s org_id=%s user_id=%s",
169
208
  job.job_id,
@@ -177,6 +216,9 @@ class DurableLearningWorker:
177
216
  job.job_id,
178
217
  job.org_id,
179
218
  )
219
+ # The persist rolled back and emit never ran, so the per-user lock is
220
+ # still held by this compute — release it so the reclaim isn't blocked.
221
+ self._release_user_lock(gen, job)
180
222
  return False
181
223
  except Exception:
182
224
  logger.exception(
@@ -185,9 +227,36 @@ class DurableLearningWorker:
185
227
  job.org_id,
186
228
  job.user_id,
187
229
  )
230
+ # emit (which releases the lock) never ran on this path — release the
231
+ # per-user lock so a failed job doesn't strand it.
232
+ self._release_user_lock(gen, job)
188
233
  # attempts was incremented by claim_learning_jobs; go dead when we've
189
234
  # exhausted max_attempts total claim attempts.
190
235
  storage.fail_learning_job(
191
236
  job_id=job.job_id, claim_token=claim_token, dead=dead
192
237
  )
193
238
  return False
239
+
240
+ def _release_user_lock(
241
+ self, gen: GenerationService | None, job: LearningJob
242
+ ) -> None:
243
+ """Best-effort release of the per-user F4 lock after a rolled-back/failed
244
+ job, so a job whose emit (the normal release site) never ran does not
245
+ strand the lock and block the same user's re-claim.
246
+
247
+ Safe to call even when compute never acquired the lock: the release is a
248
+ CAS on the holder (``clear_in_progress_lock_if_owner``) — a no-op unless
249
+ this compute's ``request_id`` still owns it. Never raises.
250
+ """
251
+ if gen is None or job.latest_request_id is None:
252
+ return
253
+ try:
254
+ gen._release_durable_learning_lock(
255
+ user_id=job.user_id, request_id=job.latest_request_id
256
+ )
257
+ except Exception:
258
+ logger.exception(
259
+ "event=learning_job_lock_release_failed job_id=%s org_id=%s",
260
+ job.job_id,
261
+ job.org_id,
262
+ )
@@ -11,7 +11,6 @@ from enum import StrEnum
11
11
 
12
12
  _FULL_MIN_DAYS = 14
13
13
  _FULL_MIN_SHADOW_N = 500
14
- _SHADOW_OFF_MIN_DAYS = 7
15
14
 
16
15
 
17
16
  class HeroState(StrEnum):
@@ -50,7 +49,7 @@ def compute_hero_state(
50
49
  migration pair in supabase/data/supabase/migrations/. The
51
50
  per-turn shadow grade in F1 lives on a different surface.)
52
51
  total_results (int): Total AgentSuccessEvaluationResult rows in the
53
- trend window (used only to differentiate EMPTY from SHADOW_OFF).
52
+ trend window, used to distinguish EMPTY from states with data.
54
53
 
55
54
  Returns:
56
55
  HeroState: The single applicable state. Empty wins over everything;
@@ -59,15 +58,7 @@ def compute_hero_state(
59
58
  if total_results == 0:
60
59
  return HeroState.EMPTY
61
60
  if not shadow_enabled:
62
- if (
63
- days_since_first_eval is not None
64
- and days_since_first_eval >= _SHADOW_OFF_MIN_DAYS
65
- ):
66
- return HeroState.SHADOW_OFF
67
- # <7 days since first eval AND shadow off → still onboarding;
68
- # render as EMPTY so the frontend shows onboarding rather than a
69
- # partly-formed trend.
70
- return HeroState.EMPTY
61
+ return HeroState.SHADOW_OFF
71
62
  if days_since_first_eval is None or days_since_first_eval < _FULL_MIN_DAYS:
72
63
  return HeroState.EARLY
73
64
  if n_shadow_in_window < _FULL_MIN_SHADOW_N:
@@ -10,7 +10,11 @@ from __future__ import annotations
10
10
  from collections.abc import Mapping
11
11
  from dataclasses import dataclass
12
12
 
13
- CitationKey = tuple[str, str] # (kind, real_id) — matches PlaybookApplicationStat
13
+ from reflexio.models.api_schema.domain import CitationKind
14
+
15
+ CitationKey = tuple[
16
+ CitationKind, str
17
+ ] # (kind, real_id) — matches PlaybookApplicationStat
14
18
  SessionIdentity = tuple[str, str] # (user_id, session_id)
15
19
 
16
20
 
@@ -19,7 +23,7 @@ class RuleAttribution:
19
23
  """One row of the "rules that moved the needle" panel."""
20
24
 
21
25
  rule_id: str
22
- kind: str
26
+ kind: CitationKind
23
27
  title: str
24
28
  successes_with: int
25
29
  failures_with: int
@@ -47,6 +47,7 @@ from reflexio.server.services.evaluation_overview.components.hero_state import (
47
47
  compute_hero_state,
48
48
  )
49
49
  from reflexio.server.services.evaluation_overview.components.rule_attribution import (
50
+ CitationKey,
50
51
  compute_net_sessions,
51
52
  )
52
53
  from reflexio.server.services.evaluation_overview.components.shadow_aggregation import (
@@ -80,14 +81,14 @@ class EvaluationOverviewService:
80
81
  Time windows used here:
81
82
  - ``[request.from_ts, request.to_ts]`` is the *trend* window. The hero
82
83
  chart, rule attribution, and the rule-attribution session set are
83
- all computed over it. The frontend sends an 8-week range so the
84
- trend chart has shape.
84
+ all computed over it. The frontend defaults to a 7-day range but
85
+ the user can widen it via the date pickers.
85
86
  - The *tile baseline* and *distribution baseline* are tighter:
86
87
  ``last_7d`` (≤ to_ts) vs ``prior_7d`` (the 7d before that).
87
- Tying these to ``request.from_ts`` is wrong — with an 8-week
88
- request, ``prior`` would land 9 weeks back and always be empty,
89
- so every tile would display "no baseline" regardless of how much
90
- data the org has.
88
+ Tying these to ``request.from_ts`` is wrong — for any window wider
89
+ than 7 days ``prior`` would land before the requested window and
90
+ always be empty, so every tile would display "no baseline"
91
+ regardless of how much data the org has.
91
92
  """
92
93
  # Tile + distribution baselines are always last-7d-vs-prior-7d,
93
94
  # anchored to ``request.to_ts`` (which is "now" from the frontend's
@@ -206,6 +207,9 @@ class EvaluationOverviewService:
206
207
  results: list[AgentSuccessEvaluationResult],
207
208
  earliest_eval_ts: int | None,
208
209
  ) -> HeroBlock:
210
+ # days_since only feeds the EARLY-vs-FULL gate, which is reachable only
211
+ # when shadow_enabled is True. Shadow is hardcoded off below, so this is
212
+ # currently inert — retained for the (pending) shadow re-enable.
209
213
  if earliest_eval_ts is None:
210
214
  days_since = None
211
215
  else:
@@ -264,8 +268,8 @@ class EvaluationOverviewService:
264
268
  def _build_attribution(
265
269
  self,
266
270
  results: list[AgentSuccessEvaluationResult],
267
- citations_by_session: dict[ResultKey, list[tuple[str, str]]],
268
- rule_titles: dict[tuple[str, str], str],
271
+ citations_by_session: dict[ResultKey, list[CitationKey]],
272
+ rule_titles: dict[CitationKey, str],
269
273
  ) -> list[RuleAttributionRow]:
270
274
  is_success_by_session = {
271
275
  (r.user_id, r.session_id): r.is_success for r in results
@@ -291,7 +295,7 @@ class EvaluationOverviewService:
291
295
 
292
296
  def _load_citations(
293
297
  self, result_keys: list[ResultKey]
294
- ) -> tuple[dict[ResultKey, list[tuple[str, str]]], dict[tuple[str, str], str]]:
298
+ ) -> tuple[dict[ResultKey, list[CitationKey]], dict[CitationKey, str]]:
295
299
  """Pull `Interaction.citations` keyed by user/session, with title lookup.
296
300
 
297
301
  Falls back to empty data when the underlying storage method returns
@@ -300,8 +304,8 @@ class EvaluationOverviewService:
300
304
  """
301
305
  wanted = set(result_keys)
302
306
  session_ids = sorted({session_id for _, session_id in result_keys})
303
- citations_by_session: dict[ResultKey, list[tuple[str, str]]] = defaultdict(list)
304
- rule_titles: dict[tuple[str, str], str] = {}
307
+ citations_by_session: dict[ResultKey, list[CitationKey]] = defaultdict(list)
308
+ rule_titles: dict[CitationKey, str] = {}
305
309
  for citation in self.storage.get_citations_by_session_ids(session_ids): # type: ignore[attr-defined]
306
310
  result_key = (citation.user_id, citation.session_id)
307
311
  if result_key not in wanted:
@@ -369,8 +373,8 @@ class EvaluationOverviewService:
369
373
  previous: list[AgentSuccessEvaluationResult],
370
374
  session_sources: dict[ResultKey, str],
371
375
  bucket: BucketLiteral,
372
- citations_by_session: dict[ResultKey, list[tuple[str, str]]],
373
- rule_titles: dict[tuple[str, str], str],
376
+ citations_by_session: dict[ResultKey, list[CitationKey]],
377
+ rule_titles: dict[CitationKey, str],
374
378
  current_scores: list[ImportedScore],
375
379
  prior_scores: list[ImportedScore],
376
380
  ) -> SourceSetComparison:
@@ -47,14 +47,25 @@ def build_extractor_agent_run_record(
47
47
  org_id: str,
48
48
  extractor_kind: str,
49
49
  user_id: str | None,
50
- request_id: str,
51
50
  agent_version: str | None,
52
51
  source: str | None,
53
52
  request_interaction_data_models: list[RequestInteractionDataModel],
54
53
  extractor_config: BaseModel,
55
54
  service_config: Any,
56
55
  agent_context: str,
56
+ generation_request_id: str | None = None,
57
+ request_id: str | None = None,
57
58
  ) -> AgentRunRecord:
59
+ if generation_request_id is not None:
60
+ if request_id is not None and request_id != generation_request_id:
61
+ raise TypeError(
62
+ "generation_request_id and request_id must match when both are provided"
63
+ )
64
+ elif request_id is not None:
65
+ generation_request_id = request_id
66
+ else:
67
+ raise TypeError("generation_request_id is required")
68
+
58
69
  source_interaction_ids = extract_source_interaction_ids(
59
70
  request_interaction_data_models
60
71
  )
@@ -66,7 +77,9 @@ def build_extractor_agent_run_record(
66
77
  org_id=org_id,
67
78
  extractor_kind=extractor_kind,
68
79
  user_id=user_id,
69
- request_id=request_id,
80
+ # The durable storage boundary still uses the legacy ``request_id``
81
+ # field name for run provenance.
82
+ request_id=generation_request_id,
70
83
  agent_version=agent_version,
71
84
  source=source,
72
85
  source_interaction_ids=source_interaction_ids,
@@ -80,7 +93,9 @@ def build_extractor_agent_run_record(
80
93
  ),
81
94
  status=AgentRunStatus.RUNNING,
82
95
  generation_request_snapshot={
83
- "request_id": request_id,
96
+ # Snapshot payloads also keep the legacy ``request_id`` key so
97
+ # existing storage readers do not need a migration.
98
+ "request_id": generation_request_id,
84
99
  "source": source,
85
100
  "source_interaction_ids": source_interaction_ids,
86
101
  "session_count": len(request_interaction_data_models),
@@ -7,6 +7,9 @@ from typing import TYPE_CHECKING, Literal
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from reflexio.server.llm.token_accounting import RunTokenTotals
10
+ from reflexio.server.services.deferred_learning_plan import (
11
+ ExtractorBookmarkAdvance,
12
+ )
10
13
 
11
14
 
12
15
  @dataclass(frozen=True)
@@ -17,6 +20,9 @@ class ExtractionOutcome[T]:
17
20
  items: list[T] = field(default_factory=list)
18
21
  run_id: str | None = None
19
22
  token_totals: RunTokenTotals | None = None
23
+ # The stride-bookmark advance the extractor no longer applies itself (F1);
24
+ # applied downstream in persist (durable) or ``.run()``'s persist half.
25
+ bookmark_advance: ExtractorBookmarkAdvance | None = None
20
26
 
21
27
  @classmethod
22
28
  def completed(
@@ -25,9 +31,14 @@ class ExtractionOutcome[T]:
25
31
  *,
26
32
  run_id: str | None = None,
27
33
  token_totals: RunTokenTotals | None = None,
34
+ bookmark_advance: ExtractorBookmarkAdvance | None = None,
28
35
  ) -> ExtractionOutcome[T]:
29
36
  return cls(
30
- status="completed", items=items, run_id=run_id, token_totals=token_totals
37
+ status="completed",
38
+ items=items,
39
+ run_id=run_id,
40
+ token_totals=token_totals,
41
+ bookmark_advance=bookmark_advance,
31
42
  )
32
43
 
33
44
  @classmethod
@@ -151,7 +151,7 @@ def format_prior_knowledge_context(matches: list[PriorAnswerMatch]) -> str | Non
151
151
  for match in resolved:
152
152
  lines.extend(
153
153
  [
154
- f"- source_request_id: {match.pending_tool_call_id}",
154
+ f"- pending_tool_call_id: {match.pending_tool_call_id}",
155
155
  f" question: {match.question_text}",
156
156
  f" answer: {_format_result(match.result)}",
157
157
  f" resolved_at: {_format_datetime(match.resolved_at)}",
@@ -161,6 +161,7 @@ def run_resumable_extraction_agent(
161
161
  log_label: str,
162
162
  ) -> AgentRunResult:
163
163
  """Run and finalize a config-gated classic extraction agent pass."""
164
+ generation_request_id = request_id
164
165
  pending_config = _pending_tool_call_config(request_context)
165
166
  storage = request_context.storage
166
167
  if storage is None:
@@ -172,7 +173,7 @@ def run_resumable_extraction_agent(
172
173
  org_id=request_context.org_id,
173
174
  extractor_kind=extractor_kind,
174
175
  user_id=user_id,
175
- request_id=request_id,
176
+ generation_request_id=generation_request_id,
176
177
  agent_version=agent_version,
177
178
  source=source,
178
179
  request_interaction_data_models=request_interaction_data_models,