fuli-context 0.9.0 → 0.11.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 (210) hide show
  1. package/README.md +29 -58
  2. package/README.zh-CN.md +24 -54
  3. package/dist/web/assets/AboutPage-CKq4IRHd.js +1 -0
  4. package/dist/web/assets/AgentConversations-DfXiFvBN.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-BBM-sKh2.js +1 -0
  7. package/dist/web/assets/AgentName-C_6R80nP.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +1 -0
  12. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-kn8MqUCO.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-D-7jLz3o.js +1 -0
  26. package/dist/web/assets/OverviewPage-BHPc-XxY.js +1 -0
  27. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  28. package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +12 -0
  34. package/dist/web/assets/ProjectAgentsPage-DNLNjYp-.css +1 -0
  35. package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +1 -0
  38. package/dist/web/assets/ReviewPage-BZhvyDWc.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-CVebTXQa.js +1 -0
  40. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  41. package/dist/web/assets/SettingsPage-1knvfOW6.js +1 -0
  42. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +1 -0
  44. package/dist/web/assets/UiDialog-Da1P10f4.js +1 -0
  45. package/dist/web/assets/UiDisclosure-8wE8YuE4.js +1 -0
  46. package/dist/web/assets/UiSegmented-BKZXc8QF.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-D5Xj5Ec4.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B8-etOBn.js +1 -0
  50. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-BfxtL9EV.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-6SN9i3WR.js} +1 -1
  54. package/dist/web/assets/client-CUTkF3t4.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CG8tdtSs.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-CyZkLJg7.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-CCMR4gDS.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-C1eN-FSB.js} +1 -1
  59. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  60. package/dist/web/assets/index-sLDKI6R-.js +5 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-l-sAqtW_.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BD1Y6TDS.js} +1 -1
  63. package/dist/web/assets/shared-report-B46dxlCR.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-DdxoKDEE.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-DlOiu8lP.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-DjqEU4W2.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +61 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_memory_routes.py +7 -0
  76. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  77. package/graph-provider/fuli_graph/runtime.py +3 -76
  78. package/graph-provider/fuli_graph/store.py +2 -0
  79. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  80. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  81. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  82. package/graph-provider/fuli_graph/store_project_agents.py +25 -124
  83. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  84. package/graph-provider/fuli_graph/system_agents.py +128 -0
  85. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  86. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  87. package/graph-provider/pyproject.toml +1 -1
  88. package/npm-shrinkwrap.json +2 -2
  89. package/package.json +2 -4
  90. package/skills/capturing-session-knowledge/SKILL.md +1 -1
  91. package/src/agent-roundtable/http.js +14 -0
  92. package/src/agent-roundtable/service.js +218 -0
  93. package/src/agent-roundtable/store.js +86 -0
  94. package/src/agent-roundtable/tools.js +61 -0
  95. package/src/agent-roundtable/wake.js +99 -0
  96. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  97. package/src/agent-tools/graph-definitions.js +3 -2
  98. package/src/agent-tools/interface-catalog.js +0 -5
  99. package/src/agent-tools.js +2 -2
  100. package/src/agents/claude-code/lifecycle-hook.js +6 -19
  101. package/src/agents/claude-code/wake.js +37 -0
  102. package/src/agents/codex/lifecycle-hook.js +51 -27
  103. package/src/agents/codex/token-usage.js +235 -0
  104. package/src/agents/codex/wake.js +47 -0
  105. package/src/agents/cursor/lifecycle-hook.js +3 -9
  106. package/src/agents/identity-receipt.js +16 -6
  107. package/src/agents/session-log.js +34 -0
  108. package/src/agents/token-usage-resolver.js +147 -0
  109. package/src/agents/wake-registry.js +14 -0
  110. package/src/cli/command-registry.js +0 -2
  111. package/src/cli/setup-command.js +2 -5
  112. package/src/cli/update-command.js +13 -2
  113. package/src/cli.js +0 -5
  114. package/src/employees/management-policy.js +4 -2
  115. package/src/employees/manifest.js +3 -0
  116. package/src/employees/recruitment.js +3 -2
  117. package/src/graphiti/agent-conversations.js +11 -9
  118. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  119. package/src/graphiti/federated-application.js +18 -6
  120. package/src/graphiti/project-agent-mapping.js +1 -1
  121. package/src/graphiti/project-agent-task-entry.js +19 -7
  122. package/src/graphiti/project-agent-workflows.js +6 -1
  123. package/src/graphiti/project-path-context.js +7 -2
  124. package/src/graphiti/provider-client.js +8 -0
  125. package/src/graphiti/repository-project-registration.js +17 -0
  126. package/src/graphiti/task-entry-preferences.js +5 -0
  127. package/src/http/api-router.js +5 -3
  128. package/src/http/error-mapping.js +0 -2
  129. package/src/mcp/instructions.js +5 -5
  130. package/src/mcp/session-id.js +1 -1
  131. package/src/mcp/tool-annotations.js +3 -2
  132. package/src/setup/agents.js +1 -41
  133. package/src/setup/graph-runtime.js +15 -2
  134. package/src/setup/session-skill.js +22 -2
  135. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  136. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  137. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  138. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  139. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  140. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  141. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  142. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  143. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  144. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  145. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  146. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  147. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  148. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  149. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  150. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  151. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  152. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  153. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  154. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  155. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  156. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  157. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  158. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  159. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  160. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  161. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  162. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  163. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  164. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  165. package/dist/web/assets/ProjectAgentsPage-BgQLIpxg.css +0 -1
  166. package/dist/web/assets/ProjectAgentsPage-CQemHFxy.js +0 -12
  167. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  168. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  169. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  170. package/dist/web/assets/RoundtablesPage-ByVeZlgC.js +0 -2
  171. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  172. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  173. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  174. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  175. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  176. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  177. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  178. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  179. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  180. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  181. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  182. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  183. package/dist/web/assets/index-UygXwZDZ.js +0 -5
  184. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  185. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  186. package/docs/roundtable-beta-testing.md +0 -102
  187. package/docs/roundtable-beta-validation.md +0 -133
  188. package/docs/roundtable-research.md +0 -142
  189. package/src/agents/a2a/roundtable-participant.js +0 -374
  190. package/src/agents/claude-code/roundtable-participant.js +0 -24
  191. package/src/agents/codex/roundtable-config.js +0 -32
  192. package/src/agents/codex/roundtable-participant.js +0 -34
  193. package/src/agents/grok/roundtable-participant.js +0 -185
  194. package/src/agents/pi/roundtable-participant.js +0 -193
  195. package/src/agents/pi/roundtable-result-format.js +0 -183
  196. package/src/cli/roundtable-command.js +0 -79
  197. package/src/roundtables/application.js +0 -69
  198. package/src/roundtables/domain.js +0 -181
  199. package/src/roundtables/http-router.js +0 -56
  200. package/src/roundtables/mcp-server.js +0 -48
  201. package/src/roundtables/participant-registry.js +0 -12
  202. package/src/roundtables/process.js +0 -70
  203. package/src/roundtables/result-contract.js +0 -47
  204. package/src/roundtables/server.js +0 -52
  205. package/src/roundtables/service.js +0 -396
  206. package/src/roundtables/store.js +0 -59
  207. package/src/roundtables/tool-contract.js +0 -41
  208. package/src/roundtables/turn-prompt.js +0 -24
  209. package/src/roundtables/worker.js +0 -85
  210. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -9,26 +9,24 @@
9
9
  />
10
10
  <link rel="icon" type="image/png" href="/assets/fuli-logo-ER1wQ8Kk.png" />
11
11
  <title>复利 · Context Graph</title>
12
- <script type="module" crossorigin src="/assets/index-UygXwZDZ.js"></script>
13
- <link rel="modulepreload" crossorigin href="/assets/i18n-D2CJkEYK.js">
14
- <link rel="modulepreload" crossorigin href="/assets/client-B2cRl-7c.js">
15
- <link rel="modulepreload" crossorigin href="/assets/console-BtPqo79j.js">
12
+ <script type="module" crossorigin src="/assets/index-sLDKI6R-.js"></script>
13
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CCMR4gDS.js">
16
14
  <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BDNMzG2s.js">
17
- <link rel="modulepreload" crossorigin href="/assets/GrowthLoading-KTjE2e50.js">
18
- <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-BtFMXFjw.js">
19
- <link rel="modulepreload" crossorigin href="/assets/vue-router-DkaTtPn0.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/client-CUTkF3t4.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/console-CG8tdtSs.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/UiDialog-Da1P10f4.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-DlOiu8lP.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/vue-router-DjqEU4W2.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/fuli-logo-Bu9WBuQe.js">
21
- <link rel="modulepreload" crossorigin href="/assets/copy-D2LqwtPZ.js">
22
- <link rel="modulepreload" crossorigin href="/assets/catalog-DN72452f.js">
23
- <link rel="modulepreload" crossorigin href="/assets/attention-store-Dk-_P4Oi.js">
24
- <link rel="modulepreload" crossorigin href="/assets/AgentHand-eGsXoGSq.js">
25
- <link rel="modulepreload" crossorigin href="/assets/profile-model-BQO4zNs9.js">
26
- <link rel="modulepreload" crossorigin href="/assets/useModalDialog-D8tiJfbc.js">
27
- <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-CWlC91Zt.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/UiSelect-D5Xj5Ec4.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/attention-store-BfxtL9EV.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/AgentHand-BBM-sKh2.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-8wE8YuE4.js">
28
25
  <link rel="modulepreload" crossorigin href="/assets/paths-CDREouhi.js">
29
- <link rel="stylesheet" crossorigin href="/assets/GrowthLoading-CIuQ1icy.css">
26
+ <link rel="stylesheet" crossorigin href="/assets/client-CIuQ1icy.css">
27
+ <link rel="stylesheet" crossorigin href="/assets/UiSelect-Bv6R0yr1.css">
30
28
  <link rel="stylesheet" crossorigin href="/assets/AgentHand-Y83Oo36U.css">
31
- <link rel="stylesheet" crossorigin href="/assets/index-CNp0tRNp.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/index-kPB0Cf8Q.css">
32
30
  </head>
33
31
  <body>
34
32
  <div id="app"></div>
@@ -7,15 +7,15 @@ Neo4j Provider 的独立日志中;工作记忆继续保存经过整理的决
7
7
 
8
8
  | 设置 | 默认 | 含义 |
9
9
  | --- | --- | --- |
10
- | 不活跃归档 | 7 天 | 按最后一条新事件计算;重试不延长活跃期 |
11
- | 自动恢复预算 | 2000 UTF-8 字节 | 保守内容上限,不是计费 token;可配 512–16000 |
10
+ | 压缩阈值 | 64 KB | 未折叠的消息原文超过阈值时,较早的消息折叠进摘要;可配 8–1024 |
11
+ | 自动恢复预算 | 4000 UTF-8 字节 | 保守内容上限,不是计费 token;可配 512–16000 |
12
12
  | 自动恢复 | 摘要 + 最近消息 | 新 chat 恢复同一 Agent、同一项目的最近上下文 |
13
- | 归档恢复 | 摘要 | 原始记录仍在数据库,可分页补读 |
14
- | 原始记录保存 | gzip 压缩 | 归档不删除原文,也不调用模型生成摘要 |
13
+ | 摘要内容 | 最近 12 个任务结果 + 最近 40 条请求要点 | 请求要点取每条用户消息的首行 |
14
+ | 原始记录保存 | gzip 压缩 | 压缩不删除原文,也不调用模型生成摘要 |
15
15
 
16
- 7 天内已成功采集的记录保持可读,不把 7 天的原文全部塞进每次提示词。摘要复用任务结束时
17
- 已经生成的工作结果;工具输出不自动注入恢复上下文。没有提交摘要的历史明确保留
18
- 空摘要,需要时补读,不生成虚构摘要。当前任务提示及宿主可见消息各有来源记录。
16
+ 压缩只看对话量,不看时间:闲置多久都不会丢失上下文。每个任务结束时的工作结果写入摘要;
17
+ 工具输出不自动注入恢复上下文。折叠后的原文仍在数据库,需要细节时用
18
+ `read_agent_conversation` 分页补读,不生成虚构摘要。当前任务提示及宿主可见消息各有来源记录。
19
19
  敏感凭据命中检测时整条内容替换为省略标记,因此“原始记录”是已过滤的可见记录。
20
20
 
21
21
  入口中的偏好保留完整指令,去掉重复证据包装。长期记忆只返回预览;更新前必须
@@ -120,7 +120,7 @@ JSONL transcript。只支持各适配器明确识别的可见文本、工具调
120
120
  ## 界面
121
121
 
122
122
  默认突出负责人、状态和当前工作;对话按需展开。执行器、路由、学习和技术明细
123
- 收在高级区域。记忆设置提供归档天数、恢复预算及采集开关,读取失败可重试。
123
+ 收在高级区域。记忆设置提供压缩阈值、恢复预算及采集开关,读取失败可重试。
124
124
 
125
125
  ## 设计参考与验证
126
126
 
@@ -128,5 +128,5 @@ JSONL transcript。只支持各适配器明确识别的可见文本、工具调
128
128
  - [Letta Agent File](https://github.com/letta-ai/agent-file):分开历史记录与送入模型的上下文。
129
129
  - [OpenHands event store](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/event_store.py):事件日志与恢复。
130
130
 
131
- 当前实现的 7 天、字节预算、能力档位、两次失败和 15 秒期限是产品配置选择,
131
+ 当前实现的压缩阈值、字节预算、能力档位、两次失败和 15 秒期限是产品配置选择,
132
132
  不是以上项目的默认配置。数据库集成测试只使用一次性本机数据库和合成数据。
@@ -0,0 +1,61 @@
1
+ # Agent Roundtable · 圆桌
2
+
3
+ Agents talk to each other directly, the way a lead asks a teammate or a sub-agent.
4
+ A Codex Agent can ask the Agent behind a specific Claude Code conversation, a Claude
5
+ Code Agent can ask a FULI Agent that has never run yet, and so on. Nobody creates a
6
+ room or invites anyone: an Agent sends a message, the recipient answers, and Fuli
7
+ records the exchange.
8
+
9
+ ## How Agents use it
10
+
11
+ All tools are on the normal `fuli` MCP server.
12
+
13
+ | Tool | Purpose |
14
+ | --- | --- |
15
+ | `find_agents` | Active FULI Agents, their clients and recent Claude Code / Codex conversations |
16
+ | `message_agent` | Ask an Agent; returns its answer, or queues the message |
17
+ | `read_agent_messages` | Messages waiting for the current task's Agent |
18
+ | `reply_agent_message` | Answer one of them |
19
+ | `read_agent_thread` | Every message of one thread |
20
+
21
+ `message_agent` takes `to` (Agent ID or exact name), `body`, the caller's
22
+ `taskContextToken`, `projectPath` (the current directory), and optionally `threadId`
23
+ to continue a thread or `conversation` to choose which of the recipient's
24
+ conversations answers (`auto` by default, `new`, or a session ID from `find_agents`).
25
+
26
+ ## Delivery
27
+
28
+ 1. **Wake.** By default Fuli wakes the recipient in its own client, headlessly and
29
+ read-only, and waits for its final answer (default 5 minutes, at most 15):
30
+ - Claude Code resumes the conversation with `--resume <id> --fork-session`, so the
31
+ answer has that conversation's full context while the original stays untouched.
32
+ - Codex runs `codex exec resume <id>` with a read-only sandbox. The question and
33
+ answer are recorded in that conversation, so it shows up there next time it is
34
+ opened. Codex refuses while the conversation is open in the app; Fuli then asks
35
+ the same Agent in a new session instead.
36
+ - Without a resumable conversation, Fuli starts a new session in the caller's
37
+ project directory with `@{agent}`, which loads that Agent's memory and role.
38
+ - If that client is not installed or not logged in on this machine, Fuli tries the
39
+ Agent's next allowed client. A timeout stops there instead of starting another run.
40
+ 2. **Inbox.** If no allowed client can answer (none installed or logged in, or the
41
+ wake timed out), the message waits. The next time that Agent starts a task,
42
+ task entry includes it under `agent_messages`, and the Agent answers with
43
+ `reply_agent_message`.
44
+
45
+ A thread holds at most 24 messages and at most 3 asks may be waiting on each other
46
+ inside one thread, so Agents cannot loop forever. Woken Agents run read-only.
47
+
48
+ ## What people see
49
+
50
+ The console's **圆桌 / Roundtable** page lists every thread with its participants and
51
+ shows the full timeline: who asked whom, through which client, the exact message and
52
+ answer, and how it was delivered. People watch; they do not need to act.
53
+
54
+ Threads are stored locally in `agent-roundtable.sqlite` in the Fuli data directory.
55
+
56
+ ## Clients
57
+
58
+ Waking needs the client's CLI on this machine: `claude` for Claude Code, and `codex`
59
+ (or the CLI bundled with the Codex app) for Codex. Override the paths with
60
+ `FULI_CLAUDE_BIN` / `FULI_CODEX_BIN`. A recipient is only woken in clients its Agent
61
+ profile allows.
@@ -1,10 +1,10 @@
1
1
  # Specialist Agents
2
2
 
3
- An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. The built-in specialists are **Jefa**, the project manager, and **Bole**, the fixed system HR Agent. Bole is created with every personal space, cannot be archived or replaced by another `hr` identity, and does not require a separate runtime package.
3
+ An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. Two roles are fixed: **Jefa**, the project manager, and **Bole**, the HR Agent. Both exist in every personal space from the start, manage all projects by default, cannot be archived or replaced by another coordinator or `hr` identity, and are never offered for hire. Neither requires a separate runtime package.
4
4
 
5
5
  ## User flow
6
6
 
7
- Open **Project Agents → Hire an Agent**, select a template and its responsibility policy, then hire. The custom project picker supports multiple selection, individual exclusions, search, and **Invert selection**. A newly recruited Jefa defaults to **All projects**, including future active projects unless explicitly excluded. **Selected projects** keeps a fixed selection; selecting none in that mode hires without a project. The Agent directory reflects the persistent identity; **Specialist Agents → Jefa** in the sidebar opens its installed board directly. Hiring alone does not create a project. Register a lasting project from the personal-project directory, or explicitly coordinate a one-off task to use the temporary scope described below.
7
+ Open **Project Agents → Hire an Agent**, select a template and its responsibility policy, then hire. Only non-fixed templates appear there; to narrow what Jefa or Bole manage, use **Manage assigned projects** on their card. The custom project picker supports multiple selection, individual exclusions, search, and **Invert selection**. **All projects** includes future active projects unless explicitly excluded. **Selected projects** keeps a fixed selection; selecting none in that mode hires without a project. The Agent directory reflects the persistent identity; **Specialist Agents → Jefa** in the sidebar opens its installed board directly. Hiring alone does not create a project. Register a lasting project from the personal-project directory, or explicitly coordinate a one-off task to use the temporary scope described below.
8
8
 
9
9
  Specialist workspaces use a compact host toolbar rather than the generic console heading. They keep the workspace's own navigation and content, plus the host's project switcher and responsibility editor. The selected project is reflected in the URL; an unauthorized explicit deep link is never silently redirected to a different project. Empty assignments, unavailable templates and catalog errors each have an actionable state. The `employee` CLI/API/manifest names are unchanged for compatibility.
10
10
 
@@ -128,7 +128,7 @@ MCP 上下文有长度上限。被截断时必须显式补读完整记忆后再
128
128
  `FULI_TEST_NEO4J_URI`。这些测试全部使用合成角色和数据;协议测试、真实数据库测试
129
129
  与真实模型客户端验收属于不同证据,不能互相替代。
130
130
 
131
- 可见对话的 7 天归档、跨客户端接续、借调与验证升级见
131
+ 可见对话的按量压缩、跨客户端接续、借调与验证升级见
132
132
  [对话与协作](agent-conversations-and-collaboration.md)。
133
133
 
134
134
  ## 必需的回复身份回执
@@ -6,10 +6,19 @@ from .model_validation import reject_credentials
6
6
 
7
7
 
8
8
  class ConversationPolicy(StrictModel):
9
- idle_days: int = Field(default=7, ge=1, le=365)
10
- context_budget: int = Field(default=2000, ge=512, le=16000)
9
+ # Fold older messages into the digest once this much unfolded text accumulates.
10
+ compact_after_kb: int = Field(default=64, ge=8, le=1024)
11
+ context_budget: int = Field(default=4000, ge=512, le=16000)
11
12
  enabled: bool = True
12
13
 
14
+ @model_validator(mode='before')
15
+ @classmethod
16
+ def drop_retired_idle_days(cls, value):
17
+ # Policies saved before size-based compaction carried an inactivity window.
18
+ if isinstance(value, dict) and 'idle_days' in value:
19
+ value = {key: item for key, item in value.items() if key != 'idle_days'}
20
+ return value
21
+
13
22
 
14
23
  class ConversationScope(StrictModel):
15
24
  personal_space_id: str = Field(min_length=1, max_length=128)
@@ -1,12 +1,43 @@
1
- """Deterministic recovery: no model calls, conservative UTF-8 byte accounting."""
1
+ """Deterministic conversation compaction and recovery: no model calls.
2
+
3
+ Older messages fold into a bounded digest once the unfolded text passes a size
4
+ threshold; raw events are always retained. Sizes are conservative UTF-8 bytes.
5
+ """
2
6
  import json
3
- from datetime import datetime, timezone
7
+
8
+ DIGEST_TASKS = 12
9
+ DIGEST_NOTES = 40
10
+ NOTE_CHARS = 160
11
+
12
+
13
+ def message_bytes(event):
14
+ if event['role'] not in ('user', 'assistant') or event.get('kind', 'message') != 'message':
15
+ return 0
16
+ return len(event['content'].encode())
17
+
18
+
19
+ def request_note(content):
20
+ """The first line of a user request, bounded; enough to recall what was asked."""
21
+ line = next((part.strip() for part in content.splitlines() if part.strip()), '')
22
+ return line if len(line) <= NOTE_CHARS else line[:NOTE_CHARS - 1] + '…'
23
+
24
+
25
+ def add_task_result(digest, at, status, summary):
26
+ tasks = [*digest.get('tasks', []), {'at': at, 'status': status, 'summary': summary}]
27
+ return {**digest, 'tasks': tasks[-DIGEST_TASKS:]}
28
+
29
+
30
+ def fold_messages(digest, events):
31
+ notes = [request_note(event['content']) for event in events
32
+ if event['role'] == 'user' and event.get('kind', 'message') == 'message']
33
+ return {**digest, 'notes': [*digest.get('notes', []), *filter(None, notes)][-DIGEST_NOTES:]}
4
34
 
5
35
 
6
- def archived(last_activity, idle_days, *, now=None):
7
- current = datetime.fromisoformat(now) if now else datetime.now(timezone.utc)
8
- previous = datetime.fromisoformat(last_activity)
9
- return (current - previous).total_seconds() >= idle_days * 86400
36
+ def render_digest(digest):
37
+ lines = [f"[{task['at'][:10]} {task['status']}] {task['summary']}" for task in digest.get('tasks', [])]
38
+ if digest.get('notes'):
39
+ lines += ['Earlier requests:', *(f'- {note}' for note in digest['notes'])]
40
+ return '\n'.join(lines)
10
41
 
11
42
 
12
43
  def pack_context(summary, events, budget):
@@ -14,12 +45,12 @@ def pack_context(summary, events, budget):
14
45
  def size():
15
46
  return len(json.dumps(result, ensure_ascii=False, separators=(',', ':')).encode())
16
47
  result['summary'] = summary or ''
17
- # Reserve half for recent messages; Unicode slicing never splits codepoints.
48
+ # Reserve half for recent messages; keep the newest digest lines.
18
49
  while len(result['summary'].encode()) > budget // 2:
19
- result['summary'] = result['summary'][:max(0, len(result['summary']) - 64)]
50
+ result['summary'] = result['summary'][64:]
20
51
  result['truncated'] = True
21
52
  for event in reversed(events):
22
- if event['role'] not in ('user', 'assistant') or event.get('kind', 'message') != 'message':
53
+ if not message_bytes(event):
23
54
  continue
24
55
  item = {'role': event['role'], 'content': event['content']}
25
56
  result['messages'].insert(0, item)
@@ -0,0 +1,88 @@
1
+ """Every task in a registered project gets a FULI Agent.
2
+
3
+ When a project has no team lead, Bole hires one through an ordinary staffing
4
+ task. The recruitment stays auditable and the new lead joins the project team.
5
+ """
6
+
7
+ from fastapi import HTTPException
8
+ from pydantic import Field
9
+
10
+ from .model_base import StrictModel
11
+ from .models import SourceApplication
12
+ from .project_agent_context_models import ProjectAgentContextResolution
13
+ from .project_agent_models import ProjectAgentProfile
14
+ from .project_agent_task_models import ProjectAgentTaskActivityCreate, ProjectAgentTaskSubmit
15
+
16
+
17
+ class DefaultProjectLeadRequest(StrictModel):
18
+ personal_space_id: str = Field(min_length=1, max_length=128)
19
+ personal_project_id: str = Field(min_length=1, max_length=128)
20
+ source_application: SourceApplication = 'other'
21
+
22
+
23
+ def default_lead_profile(project_name):
24
+ return ProjectAgentProfile(
25
+ name=f'{project_name} 负责人'[:160],
26
+ responsibility=f'负责 {project_name} 的开发、修复、测试与交付,并延续项目上下文。'[:4096],
27
+ work_kinds=['implementation', 'project_context', 'code_review', 'test_validation', 'architecture'],
28
+ capabilities=['代码实现', '问题修复', '测试验证', '代码审查', '项目上下文'],
29
+ initial_preferences=['先理解现有实现与约定,再修改。', '结论附带可复现的验证证据。'],
30
+ )
31
+
32
+
33
+ class StoreDefaultProjectLead:
34
+ async def staff_default_project_lead(self, actor, request: DefaultProjectLeadRequest):
35
+ self._require_personal()
36
+ project = await self.get_personal_project(
37
+ actor, request.personal_space_id, request.personal_project_id,
38
+ )
39
+ policy = await self.get_project_agent_coordination_policy(
40
+ actor, request.personal_space_id, request.personal_project_id,
41
+ )
42
+ if policy.team_lead_agent_id:
43
+ # An existing lead that could not take this task is a user decision.
44
+ return ProjectAgentContextResolution(status='unassigned', reason='team_lead_unavailable')
45
+ submit = ProjectAgentTaskSubmit(
46
+ personal_space_id=request.personal_space_id,
47
+ personal_project_id=request.personal_project_id,
48
+ idempotency_key=f'default-lead:{request.personal_project_id}:{policy.updated_at or "initial"}',
49
+ title='接任项目负责人',
50
+ objective='为项目配置默认负责人,承接后续任务并延续项目上下文。',
51
+ work_kind='project_context',
52
+ duration='one_off',
53
+ staffing_intent='default_lead',
54
+ source_application=request.source_application,
55
+ routing_reason='Every task needs a FULI Agent; this project had no team lead.',
56
+ recruitment_profile=default_lead_profile(project.profile.name),
57
+ )
58
+ routed = await self.submit_project_agent_task(actor, submit)
59
+ lead = routed.assigned_agent
60
+ if not lead:
61
+ return ProjectAgentContextResolution(
62
+ status='unassigned', reason=routed.task.routing_reason or 'staffing_unavailable',
63
+ )
64
+ task = routed.task
65
+ for status, summary in (('running', '接任项目负责人'), ('completed', f'{lead.profile.name} 已接任项目负责人')):
66
+ if task.status in {'completed', 'cancelled', 'failed'}:
67
+ break
68
+ try:
69
+ task = await self.record_project_agent_task_activity(actor, ProjectAgentTaskActivityCreate(
70
+ personal_space_id=request.personal_space_id,
71
+ personal_project_id=request.personal_project_id,
72
+ task_id=task.task_id,
73
+ idempotency_key=f'{submit.idempotency_key}:{status}',
74
+ status=status,
75
+ summary=summary,
76
+ agent_id=lead.agent_id,
77
+ source_application=request.source_application,
78
+ ))
79
+ except HTTPException as error:
80
+ # The hire already succeeded; a closed staffing record is cosmetic.
81
+ if error.status_code != 409:
82
+ raise
83
+ break
84
+ return ProjectAgentContextResolution(
85
+ status='ready', reason='default_project_lead',
86
+ match_basis=['HR hired the project lead for its first task'],
87
+ candidate_count=1, agent=lead,
88
+ )
@@ -13,6 +13,7 @@ from .project_agent_context_models import (
13
13
  ProjectAgentContextRequest, ProjectAgentContextResolution,
14
14
  )
15
15
  from .agent_conversation_routes import register_agent_conversation_routes
16
+ from .default_project_lead import DefaultProjectLeadRequest
16
17
  from .task_context_routes import register_task_context_routes
17
18
 
18
19
 
@@ -25,6 +26,12 @@ def register_project_agent_memory_routes(application, store, Actor):
25
26
  async def resolve_context(request: ProjectAgentContextRequest, actor: Actor):
26
27
  return await store.resolve_project_agent_context(actor, request)
27
28
 
29
+ @application.post(
30
+ '/v1/project-agent-context/default-lead', response_model=ProjectAgentContextResolution,
31
+ )
32
+ async def staff_default_lead(request: DefaultProjectLeadRequest, actor: Actor):
33
+ return await store.staff_default_project_lead(actor, request)
34
+
28
35
  @application.get(
29
36
  '/v1/project-agents/{agent_id}/memory', response_model=ProjectAgentMemoryView,
30
37
  )
@@ -20,6 +20,9 @@ from .project_agent_models import (
20
20
  ProjectAgentTaskDuration = Literal['ongoing', 'one_off']
21
21
  ProjectAgentStaffingIntent = Literal[
22
22
  'reuse_preferred',
23
+ # Like reuse_preferred, but a project without a team lead hires its first
24
+ # lead through HR without waiting for confirmation.
25
+ 'default_lead',
23
26
  'new_durable',
24
27
  'temporary',
25
28
  'unassigned',
@@ -527,83 +527,10 @@ class GraphitiRuntime:
527
527
  "MATCH (space:FuliSpace {kind: 'personal'}) RETURN space.id AS space_id",
528
528
  routing_='r',
529
529
  )
530
- if not records:
531
- return
532
- from .project_agent_models import ProjectAgentProfile
533
- from .provider_values import now_utc, stable_uuid
534
-
535
- coordinator_profile = ProjectAgentProfile(
536
- name='项目协调人',
537
- responsibility='评估任务、应用用户配置并路由已有 Agent。',
538
- agent_type='coordinator',
539
- work_kinds=['task-coordination'],
540
- capabilities=['任务评估', '策略应用', 'Agent 路由'],
541
- initial_preferences=['质量与可验收完成优先于成本和时间'],
542
- status='active',
543
- )
544
- hr_profile = ProjectAgentProfile(
545
- name='Bole',
546
- occupation_emoji='🔎',
547
- responsibility=(
548
- '维护 Agent 人员分布、当前工作与可审计招募记录,'
549
- '并在需要新角色时执行受策略约束的招募。'
550
- ),
551
- agent_type='hr',
552
- work_kinds=['agent-recruitment', 'staffing-review'],
553
- capabilities=[
554
- 'Agent 招募', '人员分布', '工作状态', '招募审计',
555
- 'fuli.employee:bole',
556
- ],
557
- initial_preferences=[
558
- '每次招募保留任务、原因、触发来源与时间线。',
559
- '只展示 Provider 已记录的人员状态,不推测未上报的工作。',
560
- ],
561
- status='active',
562
- )
563
- updated_at = now_utc()
530
+ from .system_agents import ensure_system_agents
531
+
564
532
  for record in records:
565
- space_id = record['space_id']
566
- for agent_id, profile in (
567
- ('fuli-project-coordinator', coordinator_profile),
568
- ('employee.bole', hr_profile),
569
- ):
570
- node_id = stable_uuid(
571
- self.settings.provider_id,
572
- space_id,
573
- 'project-agent',
574
- agent_id,
575
- )
576
- await self.driver.execute_query(
577
- '''
578
- MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
579
- MERGE (agent:FuliProjectAgent {id: $id})
580
- ON CREATE SET agent.agent_id = $agent_id,
581
- agent.profile_json = $profile_json,
582
- agent.name = $name,
583
- agent.occupation_emoji = $occupation_emoji,
584
- agent.responsibility = $responsibility,
585
- agent.capabilities = $capabilities,
586
- agent.work_kinds = $work_kinds,
587
- agent.agent_type = $agent_type,
588
- agent.memory_scope = 'reviewed_agent',
589
- agent.status = 'active',
590
- agent.system_managed = true,
591
- agent.created_at = $updated_at,
592
- agent.updated_at = $updated_at
593
- MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
594
- ''',
595
- space_id=space_id,
596
- id=node_id,
597
- agent_id=agent_id,
598
- profile_json=profile.model_dump_json(),
599
- name=profile.name,
600
- occupation_emoji=profile.occupation_emoji,
601
- responsibility=profile.responsibility,
602
- capabilities=profile.capabilities,
603
- work_kinds=profile.work_kinds,
604
- agent_type=profile.agent_type,
605
- updated_at=updated_at,
606
- )
533
+ await ensure_system_agents(self.driver, self.settings.provider_id, record['space_id'])
607
534
 
608
535
  async def _migrate_personal_project_relation_review_defaults(self) -> None:
609
536
  await self.driver.execute_query(
@@ -9,6 +9,7 @@ from .store_knowledge import StoreKnowledge
9
9
  from .store_project_agent_executors import StoreProjectAgentExecutors
10
10
  from .store_project_agent_memory import StoreProjectAgentMemory
11
11
  from .store_project_agent_context import StoreProjectAgentContext
12
+ from .default_project_lead import StoreDefaultProjectLead
12
13
  from .store_project_agents import StoreProjectAgents
13
14
  from .store_project_agent_attention import StoreProjectAgentAttention
14
15
  from .store_project_agent_tasks import StoreProjectAgentTasks
@@ -28,6 +29,7 @@ class GraphStore(
28
29
  StoreProjectAgentAttention,
29
30
  StoreProjectAgentMemory,
30
31
  StoreProjectAgentContext,
32
+ StoreDefaultProjectLead,
31
33
  StoreTaskContexts,
32
34
  StoreProjectAgentExecutors,
33
35
  StoreProjectAgentTasks,
@@ -5,7 +5,9 @@ import hashlib
5
5
  import json
6
6
  from fastapi import HTTPException
7
7
  from .agent_conversation_models import ConversationPolicy
8
- from .conversation_context import archived, pack_context
8
+ from .conversation_context import (
9
+ add_task_result, fold_messages, message_bytes, pack_context, render_digest,
10
+ )
9
11
  from .project_agent_access import authorize_project_agent
10
12
  from .provider_values import now_utc, stable_uuid
11
13
  from .store_transactions import query_store_transaction
@@ -77,13 +79,13 @@ class StoreAgentConversations:
77
79
  raise HTTPException(404, 'Conversation not found in this Agent and project')
78
80
  record = dict(rows[0]['c'])
79
81
  view = self._conversation_view(record, policy)
80
- if request.mode == 'context' and view['archived']:
81
- return {**view, 'context': pack_context(record.get('summary', ''), [], policy.context_budget)}
82
82
  order = 'DESC' if request.mode == 'context' else 'ASC'
83
+ # Recovery starts after the folded range; reading history still sees everything.
84
+ after = max(request.after, record.get('compacted_through') or 0) if request.mode == 'context' else request.after
83
85
  events, _, _ = await self.runtime.driver.execute_query(
84
86
  'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
85
87
  'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
86
- f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=request.after,
88
+ f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=after,
87
89
  limit=20 if request.mode == 'context' else request.limit)
88
90
  decoded = []
89
91
  total = 0
@@ -94,15 +96,52 @@ class StoreAgentConversations:
94
96
  break
95
97
  decoded.append(value)
96
98
  if request.mode == 'context':
97
- return {**view, 'context': pack_context(record.get('summary', ''), list(reversed(decoded)), policy.context_budget)}
99
+ return {**view, 'context': pack_context(self._digest_text(record), list(reversed(decoded)), policy.context_budget)}
98
100
  cursor = decoded[-1]['sequence'] if decoded else request.after
99
101
  return {**view, 'events': decoded, 'next_cursor': cursor, 'has_more': cursor < record['revision']}
100
102
 
101
103
  def _conversation_view(self, record, policy):
102
104
  return {key: record.get(key) for key in ('id', 'summary', 'status', 'revision', 'last_activity')} | {
103
- 'archived': archived(record['last_activity'], policy.idle_days),
105
+ 'compacted_through': record.get('compacted_through') or 0,
106
+ 'unfolded_bytes': record.get('unfolded_bytes') or 0,
104
107
  'policy': policy.model_dump(), 'raw_retained': True}
105
108
 
109
+ @staticmethod
110
+ def _digest_text(record):
111
+ if record.get('digest'):
112
+ return render_digest(json.loads(record['digest']))
113
+ return record.get('summary') or ''
114
+
115
+ async def _compact_conversation(self, conversation_id, policy):
116
+ """Fold older messages into the digest, keeping recent ones verbatim."""
117
+ rows, _, _ = await self.runtime.driver.execute_query(
118
+ 'MATCH (c:FuliConversation {id: $id}) RETURN c', id=conversation_id)
119
+ record = dict(rows[0]['c'])
120
+ threshold = policy.compact_after_kb * 1024
121
+ if (record.get('unfolded_bytes') or 0) <= threshold:
122
+ return
123
+ events, _, _ = await self.runtime.driver.execute_query(
124
+ 'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
125
+ 'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
126
+ 'ORDER BY e.sequence', id=conversation_id, after=record.get('compacted_through') or 0)
127
+ decoded = [decode_event(event) for event in events]
128
+ if not decoded:
129
+ return
130
+ # The newest event always stays verbatim, then as much as fits a quarter.
131
+ keep_bytes, cut = threshold // 4, len(decoded) - 1
132
+ kept = message_bytes(decoded[cut])
133
+ while cut and kept + message_bytes(decoded[cut - 1]) <= keep_bytes:
134
+ cut -= 1
135
+ kept += message_bytes(decoded[cut])
136
+ if not cut:
137
+ return
138
+ digest = json.loads(record['digest']) if record.get('digest') else {}
139
+ await self.runtime.driver.execute_query(
140
+ 'MATCH (c:FuliConversation {id: $id}) '
141
+ 'SET c.digest=$digest, c.compacted_through=$through, c.unfolded_bytes=$kept',
142
+ id=conversation_id, digest=json.dumps(fold_messages(digest, decoded[:cut]), ensure_ascii=False),
143
+ through=decoded[cut - 1]['sequence'], kept=kept)
144
+
106
145
  async def append_conversation(self, actor, request):
107
146
  if request.initialize_cursor:
108
147
  raise HTTPException(422, 'Initialize transcripts only at a verified task boundary')
@@ -137,6 +176,7 @@ class StoreAgentConversations:
137
176
  "c.last_activity=$now, c.summary='', c.status='reported' "
138
177
  'SET c.serial=coalesce(c.serial,0)+1', id=conversation_id, scope=scope, now=now)
139
178
  new_summary = False
179
+ unfolded = 0
140
180
  for event in request.events:
141
181
  digest, payload = encode_event(event)
142
182
  event_id = stable_uuid(conversation_id, request.source_application, request.session_id, event.event_id)
@@ -148,6 +188,7 @@ class StoreAgentConversations:
148
188
  continue
149
189
  if event.kind == 'checkpoint':
150
190
  new_summary = True
191
+ unfolded += message_bytes(event.model_dump())
151
192
  await store.runtime.driver.execute_query(
152
193
  'MATCH (c:FuliConversation {id: $id}) SET c.revision=c.revision+1, c.last_activity=$now '
153
194
  'CREATE (e:FuliConversationEvent {id: $event_id, digest: $digest, payload: $payload, '
@@ -155,10 +196,21 @@ class StoreAgentConversations:
155
196
  'CREATE (c)-[:HAS_EVENT]->(e)', id=conversation_id, event_id=event_id,
156
197
  digest=digest, payload=payload, now=now, source=request.source_application,
157
198
  task=request.task_context_token)
199
+ if unfolded:
200
+ await store.runtime.driver.execute_query(
201
+ 'MATCH (c:FuliConversation {id: $id}) '
202
+ 'SET c.unfolded_bytes=coalesce(c.unfolded_bytes,0)+$bytes',
203
+ id=conversation_id, bytes=unfolded)
158
204
  if request.summary is not None and new_summary:
205
+ rows, _, _ = await store.runtime.driver.execute_query(
206
+ 'MATCH (c:FuliConversation {id: $id}) RETURN c.digest AS digest', id=conversation_id)
207
+ digest = json.loads(rows[0]['digest']) if rows[0]['digest'] else {}
208
+ digest = add_task_result(digest, now, request.status or 'reported', request.summary)
159
209
  await store.runtime.driver.execute_query(
160
- 'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status',
161
- id=conversation_id, summary=request.summary, status=request.status or 'reported')
210
+ 'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status, c.digest=$digest',
211
+ id=conversation_id, summary=request.summary, status=request.status or 'reported',
212
+ digest=json.dumps(digest, ensure_ascii=False))
213
+ await store._compact_conversation(conversation_id, policy)
162
214
  if request.cursor is not None:
163
215
  await store.runtime.driver.execute_query(
164
216
  'MATCH (s:FuliTranscriptCursor {id: $id}) SET s.cursor=$cursor, s.owner_scope=$scope', id=stream, cursor=request.cursor, scope=scope)
@@ -761,9 +761,11 @@ class StoreProjectAgentTaskRecruitment:
761
761
  request.personal_space_id,
762
762
  request.personal_project_id,
763
763
  )
764
+ first_lead = (request.staffing_intent == 'default_lead' and participant_role == 'lead'
765
+ and coordination_policy.team_lead_agent_id is None)
764
766
  confirmation_mode = (
765
767
  'require_confirmation'
766
- if coordination_policy.ask_before_recruitment
768
+ if coordination_policy.ask_before_recruitment and not first_lead
767
769
  else 'automatic'
768
770
  )
769
771
  if not hr_agent_id: