fuli-context 0.11.1 → 0.12.0

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 (152) hide show
  1. package/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
  2. package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
  3. package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
  4. package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
  5. package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
  6. package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
  7. package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
  8. package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
  9. package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
  10. package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
  12. package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
  13. package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
  14. package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
  15. package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
  16. package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
  17. package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
  18. package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
  19. package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
  20. package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
  21. package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
  22. package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
  23. package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
  24. package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
  25. package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
  26. package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
  27. package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
  28. package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
  29. package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
  30. package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
  31. package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
  32. package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
  33. package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
  34. package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
  35. package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
  36. package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
  37. package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
  38. package/dist/web/assets/{attention-store-D-qIFRIb.js → attention-store-B1DI8wmd.js} +1 -1
  39. package/dist/web/assets/{catalog-UDv854JN.js → catalog-DbXv0_bR.js} +1 -1
  40. package/dist/web/assets/client-CR-W5Wzp.js +1 -0
  41. package/dist/web/assets/console-D7GHOMEC.js +1 -0
  42. package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
  43. package/dist/web/assets/{i18n-b14l2pQA.js → i18n-CVFhT_k7.js} +2 -2
  44. package/dist/web/assets/{identity-BWbPmZZf.js → identity-CM6Dmy00.js} +1 -1
  45. package/dist/web/assets/index-CJ6Jmok9.css +1 -0
  46. package/dist/web/assets/index-CRa3GnYi.js +5 -0
  47. package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
  48. package/dist/web/assets/model-WFhQZ3v_.js +1 -0
  49. package/dist/web/assets/{personal-profile-graph-BPTtMJTZ.js → personal-profile-graph-BwCh2M9G.js} +1 -1
  50. package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
  51. package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
  52. package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
  53. package/dist/web/assets/{useApi-CROJJdhE-CEQAEuug.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
  54. package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
  55. package/dist/web/index.html +16 -12
  56. package/docs/employee-agents.md +1 -1
  57. package/docs/judgment-and-autonomy.md +33 -0
  58. package/graph-provider/fuli_graph/ai_review.py +154 -0
  59. package/graph-provider/fuli_graph/common_knowledge.py +12 -0
  60. package/graph-provider/fuli_graph/graph_models.py +14 -0
  61. package/graph-provider/fuli_graph/graph_query.py +217 -0
  62. package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
  63. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
  64. package/graph-provider/fuli_graph/knowledge_management.py +23 -0
  65. package/graph-provider/fuli_graph/knowledge_review.py +383 -4
  66. package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
  67. package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
  68. package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
  69. package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
  70. package/graph-provider/fuli_graph/project_knowledge.py +41 -0
  71. package/graph-provider/fuli_graph/runtime.py +8 -0
  72. package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
  73. package/graph-provider/fuli_graph/store_knowledge.py +98 -58
  74. package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
  75. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
  76. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +2 -1
  77. package/graph-provider/fuli_graph/system_agents.py +20 -1
  78. package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
  79. package/graph-provider/pyproject.toml +1 -1
  80. package/npm-shrinkwrap.json +2 -2
  81. package/package.json +3 -2
  82. package/src/agent-tools/graph-definitions.js +2 -0
  83. package/src/agent-tools/graph-handlers.js +7 -0
  84. package/src/agent-tools/interface-catalog.js +6 -0
  85. package/src/agent-tools/judgment-definitions.js +12 -0
  86. package/src/agents/claude-code/wake.js +7 -0
  87. package/src/agents/codex/wake.js +7 -0
  88. package/src/employees/catalog/tonborg.json +20 -0
  89. package/src/employees/manifest.js +2 -2
  90. package/src/employees/recruitment.js +20 -1
  91. package/src/graphiti/agent-collaboration.js +8 -2
  92. package/src/graphiti/federated-application.js +6 -1
  93. package/src/graphiti/knowledge-review.js +7 -3
  94. package/src/graphiti/project-agent-mapping.js +2 -0
  95. package/src/graphiti/project-agent-task-entry.js +2 -2
  96. package/src/graphiti/task-entry-preferences.js +5 -0
  97. package/src/http/api-router.js +2 -0
  98. package/src/judgment/acceptance.js +91 -0
  99. package/src/judgment/engine.js +168 -0
  100. package/src/judgment/http.js +24 -0
  101. package/src/judgment/model.js +24 -0
  102. package/src/judgment/routing.js +96 -0
  103. package/src/judgment/scheduler.js +25 -0
  104. package/src/judgment/service.js +66 -0
  105. package/src/judgment/store.js +123 -0
  106. package/src/mcp/session-id.js +1 -0
  107. package/src/mcp/tool-annotations.js +4 -2
  108. package/src/server.js +9 -2
  109. package/dist/web/assets/AboutPage-cW9hDghN.js +0 -1
  110. package/dist/web/assets/AgentConversations-CGznTWuV.js +0 -2
  111. package/dist/web/assets/AgentHand-DkyF4vgk.js +0 -1
  112. package/dist/web/assets/AgentName-C1mAVr-v.js +0 -1
  113. package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +0 -1
  114. package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +0 -1
  115. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
  116. package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +0 -1
  117. package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +0 -1
  118. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
  119. package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +0 -3
  120. package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +0 -1
  121. package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +0 -1
  122. package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +0 -1
  123. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
  124. package/dist/web/assets/KnowledgePage-CQweO0YJ.js +0 -1
  125. package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +0 -4
  126. package/dist/web/assets/NotFoundPage-BmQTTqQn.js +0 -1
  127. package/dist/web/assets/OverviewPage-C73HyTsM.js +0 -1
  128. package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +0 -1
  129. package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +0 -1
  130. package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +0 -1
  131. package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +0 -12
  132. package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +0 -1
  133. package/dist/web/assets/PublicProjectsPage-DL093f65.js +0 -1
  134. package/dist/web/assets/ReviewPage-D_NIhPx8.js +0 -1
  135. package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +0 -1
  136. package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
  137. package/dist/web/assets/SettingsPage-CbLaau8Q.js +0 -1
  138. package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +0 -1
  139. package/dist/web/assets/UiDialog-Q7fsJUoO.js +0 -1
  140. package/dist/web/assets/UiDisclosure-BS-muK5j.js +0 -1
  141. package/dist/web/assets/UiSegmented-CT28bxqw.js +0 -1
  142. package/dist/web/assets/UiSelect-DcGpPQQo.js +0 -1
  143. package/dist/web/assets/WritingTastePage-CYlwAFqV.js +0 -1
  144. package/dist/web/assets/client-DLS5FZBI.js +0 -1
  145. package/dist/web/assets/console-CqzwSo7a.js +0 -1
  146. package/dist/web/assets/conversation-launcher-settings-DRoIz42V.js +0 -1
  147. package/dist/web/assets/index-D1OHIz4c.js +0 -5
  148. package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
  149. package/dist/web/assets/model-DupZy8Qb.js +0 -1
  150. package/dist/web/assets/shared-report-BYnLhmCZ.js +0 -1
  151. package/dist/web/assets/useAgentRoster-ep7SpKv1.js +0 -1
  152. package/dist/web/assets/vue-router-B6ZjK-Br.js +0 -1
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from typing import Literal
3
3
 
4
- from pydantic import Field, model_validator
4
+ from pydantic import Field, field_validator, model_validator
5
5
 
6
6
  from .models import (
7
7
  ConfirmationStatus,
@@ -11,6 +11,7 @@ from .models import (
11
11
  PersonalProfileAspect,
12
12
  PreferenceScope,
13
13
  StrictModel,
14
+ SourceApplication,
14
15
  )
15
16
 
16
17
  KnowledgeReviewScope = Literal[
@@ -23,7 +24,7 @@ KnowledgeReviewScope = Literal[
23
24
  KnowledgeReviewStatus = Literal['active', 'paused', 'completed']
24
25
  KnowledgeReviewDisposition = Literal['paused', 'completed']
25
26
  KnowledgeReviewOutcome = Literal[
26
- 'confirmed', 'updated', 'invalidated', 'deferred', 'delegated_to_ai'
27
+ 'confirmed', 'updated', 'invalidated', 'deferred', 'delegated_to_ai', 'ai_reviewed'
27
28
  ]
28
29
  KnowledgeReviewReason = Literal[
29
30
  'changed_since_last',
@@ -37,6 +38,7 @@ KnowledgeReviewReason = Literal[
37
38
  class KnowledgeReviewStart(StrictModel):
38
39
  personal_space_id: str = Field(min_length=1, max_length=128)
39
40
  scope: KnowledgeReviewScope
41
+ reviewer: Literal['human', 'tonborg'] = 'human'
40
42
  personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
41
43
 
42
44
  @model_validator(mode='after')
@@ -55,6 +57,7 @@ class KnowledgeReviewRun(StrictModel):
55
57
  scope: KnowledgeReviewScope
56
58
  personal_project_id: str | None = None
57
59
  scope_key: str
60
+ reviewer: Literal['human', 'tonborg'] = 'human'
58
61
  status: KnowledgeReviewStatus
59
62
  previous_completed_at: datetime | None = None
60
63
  review_cutoff_at: datetime
@@ -64,6 +67,25 @@ class KnowledgeReviewRun(StrictModel):
64
67
  resumed: bool = False
65
68
 
66
69
 
70
+ class KnowledgeReviewAIAssessment(StrictModel):
71
+ """The bounded, version-bound result returned by the Tonborg reviewer."""
72
+
73
+ outcome: Literal['approve', 'escalate']
74
+ summary: str = Field(min_length=1, max_length=2000)
75
+ evidence: list[str] = Field(min_length=1, max_length=8)
76
+ confidence: float = Field(ge=0, le=1)
77
+ client: SourceApplication | None = None
78
+ model: str | None = Field(default=None, min_length=1, max_length=256)
79
+ session_id: str | None = Field(default=None, min_length=1, max_length=256)
80
+
81
+ @field_validator('evidence')
82
+ @classmethod
83
+ def validate_evidence(cls, value: list[str]) -> list[str]:
84
+ if any(not item or len(item) > 1000 for item in value):
85
+ raise ValueError('AI evidence entries must contain 1 to 1000 characters')
86
+ return value
87
+
88
+
67
89
  class KnowledgeReviewCandidate(StrictModel):
68
90
  candidate_key: str
69
91
  item_id: str
@@ -90,12 +112,18 @@ class KnowledgeReviewCandidate(StrictModel):
90
112
  last_feedback_kind: KnowledgeFeedbackKind | None = None
91
113
  distinct_session_count: int = Field(ge=0)
92
114
  changed_at: datetime | None = None
115
+ ai_review_evidence_token: str = Field(
116
+ min_length=64,
117
+ max_length=64,
118
+ pattern=r'^[0-9a-f]{64}$',
119
+ )
93
120
 
94
121
 
95
122
  class KnowledgeReviewCandidateRequest(StrictModel):
96
123
  personal_space_id: str = Field(min_length=1, max_length=128)
97
124
  review_id: str = Field(min_length=1, max_length=256)
98
125
  limit: int = Field(default=10, ge=1, le=50)
126
+ offset: int = Field(default=0, ge=0, le=1000000)
99
127
 
100
128
 
101
129
  class KnowledgeReviewCandidatePage(StrictModel):
@@ -115,6 +143,28 @@ class KnowledgeReviewProgress(StrictModel):
115
143
  )
116
144
  outcome: KnowledgeReviewOutcome
117
145
  note: str | None = Field(default=None, min_length=1, max_length=2000)
146
+ ai_review_evidence_token: str | None = Field(
147
+ default=None,
148
+ min_length=64,
149
+ max_length=64,
150
+ pattern=r'^[0-9a-f]{64}$',
151
+ )
152
+ ai_assessment: KnowledgeReviewAIAssessment | None = None
153
+
154
+ @model_validator(mode='after')
155
+ def validate_ai_review_fields(self):
156
+ has_token = self.ai_review_evidence_token is not None
157
+ has_assessment = self.ai_assessment is not None
158
+ if self.outcome == 'ai_reviewed':
159
+ if not has_token or not has_assessment:
160
+ raise ValueError(
161
+ 'ai_reviewed requires an evidence token and assessment'
162
+ )
163
+ elif has_token or has_assessment:
164
+ raise ValueError(
165
+ 'AI assessment fields are only valid for ai_reviewed outcomes'
166
+ )
167
+ return self
118
168
 
119
169
 
120
170
  class KnowledgeReviewDecision(StrictModel):
@@ -125,6 +175,15 @@ class KnowledgeReviewDecision(StrictModel):
125
175
  note: str | None = None
126
176
  created_at: datetime
127
177
  updated_at: datetime
178
+ ai_review_evidence_token: str | None = Field(
179
+ default=None,
180
+ min_length=64,
181
+ max_length=64,
182
+ pattern=r'^[0-9a-f]{64}$',
183
+ )
184
+ ai_assessment: KnowledgeReviewAIAssessment | None = None
185
+ ai_review_scope: KnowledgeReviewScope | None = None
186
+ ai_review_project_id: str | None = Field(default=None, min_length=1, max_length=128)
128
187
 
129
188
 
130
189
  class KnowledgeReviewFinish(StrictModel):
@@ -1116,7 +1116,9 @@ OPTIONAL MATCH (old_assertion:Entity {
1116
1116
  })
1117
1117
  SET preview.used_at = $changed_at
1118
1118
  FOREACH (_ IN CASE WHEN old_assertion IS NULL THEN [] ELSE [1] END |
1119
- SET old_assertion.fuli_invalid_at = $changed_at,
1119
+ SET old_assertion.fuli_ai_review_lock_version =
1120
+ coalesce(old_assertion.fuli_ai_review_lock_version, 0) + 1,
1121
+ old_assertion.fuli_invalid_at = $changed_at,
1120
1122
  old_assertion.expired_at = $changed_at
1121
1123
  )
1122
1124
  SET current.candidate_version = $candidate_version,
@@ -1176,6 +1178,8 @@ FOREACH (_ IN CASE WHEN $global_assertion_id IS NULL THEN [] ELSE [1] END |
1176
1178
  assertion.fuli_distinct_task_count = 0,
1177
1179
  assertion.fuli_usage_generation = 1,
1178
1180
  assertion.fuli_invalid_at = null
1181
+ SET assertion.fuli_ai_review_lock_version =
1182
+ coalesce(assertion.fuli_ai_review_lock_version, 0) + 1
1179
1183
  MERGE (episode:Episodic {uuid: $episode_id})
1180
1184
  ON CREATE SET episode.group_id = $group_id,
1181
1185
  episode.name = 'Personal-global preference human review',
@@ -19,6 +19,18 @@ from .models import (
19
19
  from .provider_values import native_datetime as _native_datetime
20
20
 
21
21
 
22
+ def _item_match(item_kind: str, variable: str, id_parameter: str) -> str:
23
+ if item_kind == 'entity':
24
+ return (
25
+ f'MATCH ({variable}:Entity '
26
+ f'{{uuid: ${id_parameter}, group_id: $group_id}})'
27
+ )
28
+ return (
29
+ f'MATCH ()-[{variable}:RELATES_TO '
30
+ f'{{uuid: ${id_parameter}, group_id: $group_id}}]->()'
31
+ )
32
+
33
+
22
34
  async def defer_preference_conflict(
23
35
  store,
24
36
  actor: dict,
@@ -44,9 +56,42 @@ async def defer_preference_conflict(
44
56
  )
45
57
  _validate_pair_scope(left, right, request)
46
58
  deferred_at = datetime.now(timezone.utc)
59
+ left_match = _item_match(
60
+ request.left_item_kind, 'left', 'left_item_id'
61
+ )
62
+ right_match = _item_match(
63
+ request.right_item_kind, 'right', 'right_item_id'
64
+ )
47
65
  records, _, _ = await store.runtime.driver.execute_query(
48
66
  '''
49
67
  MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
68
+ '''
69
+ + left_match
70
+ + '\n '
71
+ + right_match
72
+ + '''
73
+ WITH space,
74
+ CASE
75
+ WHEN $left_item_kind + ':' + left.uuid =
76
+ $right_item_kind + ':' + right.uuid
77
+ THEN [{kind: $left_item_kind, item: left}]
78
+ WHEN $left_item_kind + ':' + left.uuid <
79
+ $right_item_kind + ':' + right.uuid
80
+ THEN [
81
+ {kind: $left_item_kind, item: left},
82
+ {kind: $right_item_kind, item: right}
83
+ ]
84
+ ELSE [
85
+ {kind: $right_item_kind, item: right},
86
+ {kind: $left_item_kind, item: left}
87
+ ]
88
+ END AS lock_items
89
+ UNWIND lock_items AS lock_item
90
+ WITH space, lock_item
91
+ ORDER BY lock_item.kind + ':' + lock_item.item.uuid
92
+ SET lock_item.item.fuli_ai_review_lock_version =
93
+ coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
94
+ WITH space, collect(lock_item) AS _locked_items
50
95
  MERGE (conflict:FuliPreferenceConflict {id: $conflict_id})
51
96
  ON CREATE SET conflict.personal_space_id = $space_id,
52
97
  conflict.preference_key = $preference_key,
@@ -70,6 +115,7 @@ async def defer_preference_conflict(
70
115
  RETURN conflict
71
116
  ''',
72
117
  space_id=space['id'],
118
+ group_id=space['group_id'],
73
119
  conflict_id=request.conflict_id,
74
120
  preference_key=request.preference_key,
75
121
  preference_scope=request.preference_scope,
@@ -160,10 +206,12 @@ async def resolve_preference_conflict(
160
206
  return await _complete(
161
207
  store,
162
208
  space['id'],
209
+ space['group_id'],
163
210
  conflict_id,
164
211
  request.resolution,
165
212
  request.reason,
166
213
  request.operation_actor,
214
+ conflict,
167
215
  )
168
216
 
169
217
 
@@ -179,14 +227,16 @@ async def complete_preference_conflict(
179
227
  request.personal_space_id,
180
228
  'maintainer',
181
229
  )
182
- await _read_conflict(store, space['id'], conflict_id)
230
+ conflict = await _read_conflict(store, space['id'], conflict_id)
183
231
  return await _complete(
184
232
  store,
185
233
  space['id'],
234
+ space['group_id'],
186
235
  conflict_id,
187
236
  request.resolution,
188
237
  request.reason,
189
238
  request.operation_actor,
239
+ conflict,
190
240
  )
191
241
 
192
242
 
@@ -305,17 +355,52 @@ async def _split_scope(store, actor, space, conflict, left, right, request):
305
355
  async def _complete(
306
356
  store,
307
357
  space_id,
358
+ group_id,
308
359
  conflict_id,
309
360
  resolution,
310
361
  reason,
311
362
  resolved_by,
363
+ conflict: PreferenceConflictRecord,
312
364
  ):
313
365
  resolved_at = datetime.now(timezone.utc)
366
+ left_match = _item_match(
367
+ conflict.left_item_kind, 'left', 'left_item_id'
368
+ )
369
+ right_match = _item_match(
370
+ conflict.right_item_kind, 'right', 'right_item_id'
371
+ )
314
372
  records, _, _ = await store.runtime.driver.execute_query(
315
373
  '''
316
374
  MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
317
375
  [:HAS_PREFERENCE_CONFLICT]->
318
376
  (conflict:FuliPreferenceConflict {id: $conflict_id})
377
+ '''
378
+ + left_match
379
+ + '\n '
380
+ + right_match
381
+ + '''
382
+ WITH conflict,
383
+ CASE
384
+ WHEN $left_item_kind + ':' + left.uuid =
385
+ $right_item_kind + ':' + right.uuid
386
+ THEN [{kind: $left_item_kind, item: left}]
387
+ WHEN $left_item_kind + ':' + left.uuid <
388
+ $right_item_kind + ':' + right.uuid
389
+ THEN [
390
+ {kind: $left_item_kind, item: left},
391
+ {kind: $right_item_kind, item: right}
392
+ ]
393
+ ELSE [
394
+ {kind: $right_item_kind, item: right},
395
+ {kind: $left_item_kind, item: left}
396
+ ]
397
+ END AS lock_items
398
+ UNWIND lock_items AS lock_item
399
+ WITH conflict, lock_item
400
+ ORDER BY lock_item.kind + ':' + lock_item.item.uuid
401
+ SET lock_item.item.fuli_ai_review_lock_version =
402
+ coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
403
+ WITH conflict, collect(lock_item) AS _locked_items
319
404
  SET conflict.status = 'resolved',
320
405
  conflict.resolution = $resolution,
321
406
  conflict.resolved_by = $resolved_by,
@@ -325,7 +410,12 @@ async def _complete(
325
410
  RETURN conflict
326
411
  ''',
327
412
  space_id=space_id,
413
+ group_id=group_id,
328
414
  conflict_id=conflict_id,
415
+ left_item_id=conflict.left_item_id,
416
+ left_item_kind=conflict.left_item_kind,
417
+ right_item_id=conflict.right_item_id,
418
+ right_item_kind=conflict.right_item_kind,
329
419
  resolution=resolution,
330
420
  resolved_by=resolved_by,
331
421
  reason=reason,
@@ -494,6 +494,8 @@ class ProjectAgentTaskRecord(StrictModel):
494
494
 
495
495
  class ProjectAgentTaskActivityCreate(StrictModel):
496
496
  artifact_revision: str | None = Field(default=None, min_length=1, max_length=160)
497
+ expected_verification_attempt_id: str | None = Field(default=None, min_length=1, max_length=256)
498
+ judgment_task_context_token: str | None = Field(default=None, min_length=1, max_length=256)
497
499
  personal_space_id: str = Field(min_length=1, max_length=128)
498
500
  personal_project_id: str = Field(min_length=1, max_length=128)
499
501
  task_id: str = Field(min_length=1, max_length=128)
@@ -578,11 +578,25 @@ async def _upsert_reference(
578
578
  space['id'], 'knowledge-reference', item_kind, item_id, project_id
579
579
  )
580
580
  changed_at = datetime.now(timezone.utc)
581
+ item_match = (
582
+ 'MATCH (item:Entity {uuid: $item_id, group_id: $group_id})'
583
+ if item_kind == 'entity'
584
+ else (
585
+ 'MATCH ()-[item:RELATES_TO '
586
+ '{uuid: $item_id, group_id: $group_id}]->()'
587
+ )
588
+ )
581
589
  records, _, _ = await store.runtime.driver.execute_query(
582
590
  '''
583
591
  MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
584
592
  MATCH (space)-[:CONTAINS_PROJECT]->
585
593
  (target:FuliPersonalProject {project_id: $project_id})
594
+ '''
595
+ + item_match
596
+ + '''
597
+ SET item.fuli_ai_review_lock_version =
598
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
599
+ WITH space, target
586
600
  MERGE (reference:FuliKnowledgeProjectReference {
587
601
  space_id: $space_id,
588
602
  item_kind: $item_kind,
@@ -604,6 +618,7 @@ async def _upsert_reference(
604
618
  space_id=space['id'],
605
619
  item_kind=item_kind,
606
620
  item_id=item_id,
621
+ group_id=space['group_id'],
607
622
  project_id=project_id,
608
623
  reference_id=reference_id,
609
624
  source_project_id=source_project_id,
@@ -635,6 +650,31 @@ async def _upsert_conflict(
635
650
  records, _, _ = await store.runtime.driver.execute_query(
636
651
  '''
637
652
  MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
653
+ MATCH (item:Entity {uuid: $item_id, group_id: $group_id})
654
+ MATCH (target_item:Entity {
655
+ uuid: $target_item_id,
656
+ group_id: $group_id
657
+ })
658
+ WITH space,
659
+ CASE
660
+ WHEN item.uuid = target_item.uuid
661
+ THEN [{kind: 'entity', item: item}]
662
+ WHEN item.uuid < target_item.uuid
663
+ THEN [
664
+ {kind: 'entity', item: item},
665
+ {kind: 'entity', item: target_item}
666
+ ]
667
+ ELSE [
668
+ {kind: 'entity', item: target_item},
669
+ {kind: 'entity', item: item}
670
+ ]
671
+ END AS lock_items
672
+ UNWIND lock_items AS lock_item
673
+ WITH space, lock_item
674
+ ORDER BY lock_item.kind + ':' + lock_item.item.uuid
675
+ SET lock_item.item.fuli_ai_review_lock_version =
676
+ coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
677
+ WITH space, collect(lock_item) AS _locked_items
638
678
  MERGE (conflict:FuliKnowledgeConflict {id: $conflict_id})
639
679
  ON CREATE SET conflict.created_at = $changed_at
640
680
  SET conflict.space_id = $space_id,
@@ -651,6 +691,7 @@ async def _upsert_conflict(
651
691
  RETURN conflict
652
692
  ''',
653
693
  space_id=space['id'],
694
+ group_id=space['group_id'],
654
695
  conflict_id=conflict_id,
655
696
  item_id=item_id,
656
697
  target_item_id=target_item_id,
@@ -656,6 +656,10 @@ class GraphitiRuntime:
656
656
  OR item.fuli_usage_generation IS NULL
657
657
  OR item.fuli_inheritance_mode IS NULL
658
658
  OR item.fuli_inherited_project_ids IS NULL
659
+ WITH item
660
+ ORDER BY 'entity:' + item.uuid
661
+ SET item.fuli_ai_review_lock_version =
662
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
659
663
  SET item.fuli_utility_score =
660
664
  coalesce(item.fuli_utility_score, 0.0),
661
665
  item.fuli_confidence_score = coalesce(
@@ -688,6 +692,10 @@ class GraphitiRuntime:
688
692
  OR item.fuli_usage_generation IS NULL
689
693
  OR item.fuli_inheritance_mode IS NULL
690
694
  OR item.fuli_inherited_project_ids IS NULL
695
+ WITH item
696
+ ORDER BY 'relationship:' + item.uuid
697
+ SET item.fuli_ai_review_lock_version =
698
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
691
699
  SET item.fuli_utility_score =
692
700
  coalesce(item.fuli_utility_score, 0.0),
693
701
  item.fuli_confidence_score = coalesce(
@@ -43,7 +43,7 @@ class StoreAgentVerification:
43
43
  task = await self._quality_task(actor, request)
44
44
  rows, _, _ = await self.runtime.driver.execute_query(
45
45
  'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space})-[:HAS_VERIFICATION]->(v:FuliAgentVerification) '
46
- 'RETURN v.record_json AS record_json ORDER BY v.sequence', task=request.task_id, space=request.personal_space_id)
46
+ 'RETURN v.record_json AS record_json, v.run_id AS run_id ORDER BY v.sequence', task=request.task_id, space=request.personal_space_id)
47
47
  models = await self._quality_models(actor, request, task)
48
48
  result = evaluate_quality_policy(complexity=task.get('complexity') or 'standard',
49
49
  current_strategy=json.loads(task.get('effective_model_strategy_json') or '{"mode":"adaptive"}'),
@@ -53,7 +53,12 @@ class StoreAgentVerification:
53
53
  if selected:
54
54
  selected['executor_id'] = next(model['executor_id'] for model in models
55
55
  if all(model.get(key) == selected[key] for key in ('provider', 'model', 'capability_tier', 'cost_rank')))
56
- return {**result, 'evidence_authority': 'host_reported_test_evidence',
56
+ verified = next((row for row in rows if json.loads(row['record_json']).get('attempt_id') == result['verified_attempt_id']), None)
57
+ attempt = json.loads(verified['record_json']) if verified else None
58
+ if attempt:
59
+ attempt = {key: attempt.get(key) for key in ('attempt_id', 'artifact_revision', 'outcome', 'evidence_refs', 'verifier_id', 'executor_id', 'provider', 'model')}
60
+ attempt['run_id'] = verified.get('run_id')
61
+ return {**result, 'verified_attempt': attempt, 'evidence_authority': 'host_reported_test_evidence',
57
62
  'execution_started': False, 'guidance': 'Only complete the task after passed for the current artifact revision. Blocked means no authorized preflighted model proves the required capability tier. Model selection does not prove execution.'}
58
63
 
59
64
  async def record_agent_verification(self, actor, request):
@@ -92,7 +97,8 @@ class StoreAgentVerification:
92
97
  raise HTTPException(409, 'An execution run cannot count as two verification attempts')
93
98
  identifier = stable_uuid('verification', request.personal_space_id, request.task_id, request.attempt_id)
94
99
  record = dict(attempt_id=request.attempt_id, outcome=request.outcome, artifact_revision=request.artifact_revision,
95
- evidence_refs=request.evidence_refs, capability_tier=observed['capability_tier'], verifier_id=context['project_agent_id'])
100
+ evidence_refs=request.evidence_refs, capability_tier=observed['capability_tier'], verifier_id=context['project_agent_id'],
101
+ executor_id=request.executor_id, provider=request.provider, model=request.model)
96
102
  fingerprint = store._payload_hash(request)
97
103
  rows, _, _ = await store.runtime.driver.execute_query(
98
104
  'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space}) '
@@ -106,6 +112,7 @@ class StoreAgentVerification:
106
112
  result = await store.query_agent_quality(actor, request)
107
113
  await store.runtime.driver.execute_query(
108
114
  'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space}) '
109
- 'SET t.quality_gate=$status, t.verified_artifact_revision=$revision',
110
- task=request.task_id, space=request.personal_space_id, status=result['status'], revision=request.artifact_revision)
115
+ 'SET t.quality_gate=$status, t.verified_artifact_revision=$revision, t.verified_attempt_id=$attempt',
116
+ task=request.task_id, space=request.personal_space_id, status=result['status'], revision=request.artifact_revision,
117
+ attempt=result.get('verified_attempt_id'))
111
118
  return result
@@ -325,6 +325,8 @@ class StoreKnowledge:
325
325
  await self.runtime.driver.execute_query(
326
326
  '''
327
327
  UNWIND $entities AS row
328
+ WITH row
329
+ ORDER BY 'entity:' + row.uuid
328
330
  MERGE (entity:Entity {uuid: row.uuid})
329
331
  ON CREATE SET entity.group_id = $group_id,
330
332
  entity.name = row.name,
@@ -365,6 +367,8 @@ class StoreKnowledge:
365
367
  entity.fuli_preference_project_id = row.preference_project_id,
366
368
  entity.fuli_preference_agent_id = row.preference_agent_id,
367
369
  entity.fuli_attributes_json = row.attributes_json
370
+ SET entity.fuli_ai_review_lock_version =
371
+ coalesce(entity.fuli_ai_review_lock_version, 0) + 1
368
372
  WITH count(entity) AS entity_count
369
373
  CREATE (episode:Episodic {
370
374
  uuid: $episode_id,
@@ -392,12 +396,95 @@ class StoreKnowledge:
392
396
  })
393
397
  WITH episode
394
398
  UNWIND $entities AS row
399
+ WITH episode, row
400
+ ORDER BY 'entity:' + row.uuid
395
401
  MATCH (entity:Entity {uuid: row.uuid})
396
402
  MERGE (episode)-[mention:MENTIONS {uuid: row.mention_uuid}]->(entity)
397
403
  SET mention.group_id = $group_id,
398
404
  mention.created_at = $created_at
399
405
  WITH episode
406
+ CALL {
407
+ WITH episode
408
+ OPTIONAL MATCH ()-[old:RELATES_TO {group_id: $group_id}]->()
409
+ WITH collect(old) AS candidate_olds
410
+ UNWIND candidate_olds AS old
411
+ WITH old
412
+ WHERE old IS NOT NULL
413
+ AND old.invalid_at IS NULL
414
+ AND NOT (old.uuid IN $relationship_ids)
415
+ AND (
416
+ EXISTS {
417
+ MATCH (:FuliSpace {id: $space_id})-
418
+ [:HAS_KNOWLEDGE_ASSIGNMENT]->
419
+ (assignment:FuliKnowledgeAssignment {
420
+ item_kind: 'relationship',
421
+ item_id: old.uuid
422
+ })
423
+ WHERE assignment.project_id = $personal_project_id
424
+ }
425
+ OR (
426
+ NOT EXISTS {
427
+ MATCH (:FuliSpace {id: $space_id})-
428
+ [:HAS_KNOWLEDGE_ASSIGNMENT]->
429
+ (:FuliKnowledgeAssignment {
430
+ item_kind: 'relationship',
431
+ item_id: old.uuid
432
+ })
433
+ }
434
+ AND EXISTS {
435
+ MATCH (old_episode:Episodic {group_id: $group_id})
436
+ WHERE old_episode.uuid IN coalesce(old.episodes, [])
437
+ AND (
438
+ old_episode.fuli_personal_project_id =
439
+ $personal_project_id
440
+ OR (
441
+ old_episode.fuli_personal_project_id IS NULL
442
+ AND $personal_project_id IS NULL
443
+ )
444
+ )
445
+ }
446
+ )
447
+ )
448
+ AND EXISTS {
449
+ MATCH (agent_episode:Episodic {group_id: $group_id})
450
+ WHERE agent_episode.uuid IN coalesce(old.episodes, [])
451
+ AND (
452
+ agent_episode.fuli_project_agent_id = $project_agent_id
453
+ OR (
454
+ agent_episode.fuli_project_agent_id IS NULL
455
+ AND $project_agent_id IS NULL
456
+ )
457
+ )
458
+ }
459
+ AND any(
460
+ replacement IN $superseded_relationships
461
+ WHERE replacement.key = old.fuli_key
462
+ )
463
+ WITH collect(old) AS superseded_edges
464
+ OPTIONAL MATCH ()-[existing:RELATES_TO {
465
+ group_id: $group_id
466
+ }]->()
467
+ WITH superseded_edges, collect(existing) AS candidate_existing
468
+ RETURN superseded_edges,
469
+ [edge IN candidate_existing
470
+ WHERE edge IS NOT NULL
471
+ AND edge.uuid IN $relationship_ids]
472
+ AS existing_edges
473
+ }
474
+ CALL {
475
+ WITH superseded_edges, existing_edges
476
+ UNWIND superseded_edges + existing_edges AS lock_edge
477
+ WITH lock_edge
478
+ WHERE lock_edge IS NOT NULL
479
+ ORDER BY 'relationship:' + lock_edge.uuid
480
+ SET lock_edge.fuli_ai_review_lock_version =
481
+ coalesce(lock_edge.fuli_ai_review_lock_version, 0) + 1
482
+ RETURN count(*) AS _locked_relationship_count
483
+ }
484
+ WITH episode, superseded_edges
400
485
  UNWIND $relationships AS row
486
+ WITH episode, superseded_edges, row
487
+ ORDER BY 'relationship:' + row.uuid
401
488
  MATCH (source:Entity {uuid: row.source_uuid})
402
489
  MATCH (target:Entity {uuid: row.target_uuid})
403
490
  MERGE (source)-[edge:RELATES_TO {uuid: row.uuid}]->(target)
@@ -435,73 +522,26 @@ class StoreKnowledge:
435
522
  edge.fuli_workflow_confirmation_authority =
436
523
  row.workflow_confirmation_authority,
437
524
  edge.fuli_workflow_session_authority =
438
- row.workflow_session_authority
525
+ row.workflow_session_authority,
526
+ edge.fuli_ai_review_lock_version = 1
439
527
  SET edge.episodes =
440
528
  CASE WHEN $episode_id IN coalesce(edge.episodes, [])
441
529
  THEN edge.episodes
442
530
  ELSE coalesce(edge.episodes, []) + $episode_id END
443
- WITH count(edge) AS edge_count
444
- OPTIONAL MATCH ()-[old:RELATES_TO {group_id: $group_id}]->()
445
- WHERE old.invalid_at IS NULL
446
- AND NOT (old.uuid IN $relationship_ids)
447
- AND (
448
- EXISTS {
449
- MATCH (:FuliSpace {id: $space_id})-
450
- [:HAS_KNOWLEDGE_ASSIGNMENT]->
451
- (assignment:FuliKnowledgeAssignment {
452
- item_kind: 'relationship',
453
- item_id: old.uuid
454
- })
455
- WHERE assignment.project_id = $personal_project_id
456
- }
457
- OR (
458
- NOT EXISTS {
459
- MATCH (:FuliSpace {id: $space_id})-
460
- [:HAS_KNOWLEDGE_ASSIGNMENT]->
461
- (:FuliKnowledgeAssignment {
462
- item_kind: 'relationship',
463
- item_id: old.uuid
464
- })
465
- }
466
- AND EXISTS {
467
- MATCH (old_episode:Episodic {group_id: $group_id})
468
- WHERE old_episode.uuid IN coalesce(old.episodes, [])
469
- AND (
470
- old_episode.fuli_personal_project_id =
471
- $personal_project_id
472
- OR (
473
- old_episode.fuli_personal_project_id IS NULL
474
- AND $personal_project_id IS NULL
475
- )
476
- )
477
- }
478
- )
479
- )
480
- AND EXISTS {
481
- MATCH (agent_episode:Episodic {group_id: $group_id})
482
- WHERE agent_episode.uuid IN coalesce(old.episodes, [])
483
- AND (
484
- agent_episode.fuli_project_agent_id = $project_agent_id
485
- OR (
486
- agent_episode.fuli_project_agent_id IS NULL
487
- AND $project_agent_id IS NULL
488
- )
489
- )
490
- }
491
- AND any(
531
+ WITH episode, superseded_edges, count(edge) AS edge_count
532
+ CALL {
533
+ WITH superseded_edges
534
+ UNWIND superseded_edges AS old
535
+ WITH old, head([
492
536
  replacement IN $superseded_relationships
493
- WHERE replacement.key = old.fuli_key
494
- )
495
- WITH edge_count, old, head([
496
- replacement IN $superseded_relationships
497
- WHERE replacement.key = old.fuli_key | replacement
498
- ]) AS replacement
499
- FOREACH (_ IN CASE WHEN old IS NULL THEN [] ELSE [1] END |
537
+ WHERE replacement.key = old.fuli_key | replacement
538
+ ]) AS replacement
500
539
  SET old.invalid_at = $reference_time,
501
540
  old.expired_at = $created_at,
502
541
  old.fuli_replaced_by_item_id = replacement.replacement_id,
503
542
  old.fuli_replaced_by_item_kind = 'relationship'
504
- )
543
+ RETURN count(*) AS _superseded_relationship_count
544
+ }
505
545
  RETURN edge_count
506
546
  ''',
507
547
  entities=[
@@ -24,7 +24,7 @@ class StoreProjectAgentAttention:
24
24
  self, actor, space, request.personal_project_id, agent_id,
25
25
  require_active=True,
26
26
  allow_unassigned=agent_id in {
27
- 'employee.bole', 'employee.jefa', 'fuli-project-coordinator',
27
+ 'employee.bole', 'employee.jefa', 'employee.tonborg', 'fuli-project-coordinator',
28
28
  },
29
29
  )
30
30
  if request.task_id: