@scemoon/cdh 1.0.4 → 1.0.6

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 (269) hide show
  1. package/ai-dlc-skill/SKILL.md +4 -2
  2. package/ai-dlc-skill/agents/master.md +6 -0
  3. package/ai-dlc-skill/agents/plan-agent.md +2 -2
  4. package/ai-dlc-skill/agents/understand-agent.md +4 -4
  5. package/ai-dlc-skill/agents/verify-agent.md +1 -1
  6. package/ai-dlc-skill/architecture/project-structure.md +10 -10
  7. package/ai-dlc-skill/brownfield/README.md +2 -2
  8. package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
  9. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
  10. package/ai-dlc-skill/components/backend.md +1 -1
  11. package/ai-dlc-skill/components/web.md +2 -2
  12. package/ai-dlc-skill/contracts/README.md +3 -3
  13. package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
  14. package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
  15. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
  16. package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
  17. package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
  18. package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
  19. package/ai-dlc-skill/phases/deliver/rules.md +1 -1
  20. package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
  21. package/ai-dlc-skill/phases/plan/prompt.md +6 -6
  22. package/ai-dlc-skill/phases/plan/rules.md +1 -1
  23. package/ai-dlc-skill/phases/understand/entry.md +1 -1
  24. package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
  25. package/ai-dlc-skill/phases/understand/prompt.md +8 -8
  26. package/ai-dlc-skill/phases/understand/rules.md +5 -5
  27. package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
  28. package/ai-dlc-skill/phases/verify/prompt.md +8 -8
  29. package/ai-dlc-skill/phases/verify/rules.md +9 -9
  30. package/ai-dlc-skill/providers/README.md +2 -2
  31. package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
  32. package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
  33. package/ai-dlc-skill/requirements.md +7 -7
  34. package/ai-dlc-skill/skill.yaml +4 -4
  35. package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
  36. package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
  37. package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
  38. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
  39. package/ai-dlc-skill/templates/project/README.md +18 -19
  40. package/ai-dlc-skill/templates/project/template.md +21 -21
  41. package/ai-dlc-skill/walkthrough/collect.sh +1 -1
  42. package/ai-dlc-skill/walkthrough/template.md +2 -2
  43. package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
  44. package/ai-dlc-skill/workflows/deployment.yaml +4 -4
  45. package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
  46. package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
  47. package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
  48. package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
  49. package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
  50. package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
  51. package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
  52. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  53. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  54. package/cdh/cdh_mcp_injector.py +115 -0
  55. package/cdh/cdh_mcp_loader.py +67 -0
  56. package/cdh/cdh_mcp_manager.py +73 -0
  57. package/cdh/cdh_session_aggregator.py +258 -0
  58. package/cdh/cdh_skill_manager.py +105 -0
  59. package/cdh/cli.py +64 -4
  60. package/cdh/scaffold.py +192 -41
  61. package/onecode/__init__.py +1 -1
  62. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  63. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  64. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  65. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  66. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  67. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  68. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  69. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  70. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  71. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  72. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  73. package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
  74. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  75. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  76. package/onecode/agent/agents/types.py +47 -164
  77. package/onecode/agent/attachments.py +0 -1
  78. package/onecode/agent/cdh_loader.py +17 -25
  79. package/onecode/agent/context.py +8 -3
  80. package/onecode/agent/engine.py +835 -220
  81. package/onecode/agent/hooks.py +1 -1
  82. package/onecode/agent/onecode_agent_acp.py +460 -47
  83. package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
  84. package/onecode/agent/permissions.py +0 -1
  85. package/onecode/agent/project_doc.py +19 -0
  86. package/onecode/agent/session.py +17 -3
  87. package/onecode/agent/snapshot.py +3 -4
  88. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  89. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  90. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  91. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  92. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  93. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  94. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  95. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  96. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  97. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  98. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  99. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  100. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  101. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  102. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  103. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  104. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  105. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  106. package/onecode/agent/tools/agent_tools.py +4 -6
  107. package/onecode/agent/tools/apply_patch_tool.py +2 -8
  108. package/onecode/agent/tools/bash_tool.py +32 -12
  109. package/onecode/agent/tools/communication_tools.py +4 -5
  110. package/onecode/agent/tools/config_tool.py +3 -3
  111. package/onecode/agent/tools/cron_tools.py +4 -5
  112. package/onecode/agent/tools/file_ops.py +37 -3
  113. package/onecode/agent/tools/file_tools.py +10 -11
  114. package/onecode/agent/tools/git_tools.py +2 -2
  115. package/onecode/agent/tools/lsp_tools.py +2 -2
  116. package/onecode/agent/tools/mcp_tools.py +3 -5
  117. package/onecode/agent/tools/permission_handler.py +1 -1
  118. package/onecode/agent/tools/protocol.py +1 -1
  119. package/onecode/agent/tools/registry.py +69 -2
  120. package/onecode/agent/tools/sandbox.py +110 -16
  121. package/onecode/agent/tools/skill_tools.py +2 -2
  122. package/onecode/agent/tools/todo_tools.py +38 -19
  123. package/onecode/agent/tools/web_tools.py +6 -7
  124. package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
  125. package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
  126. package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
  127. package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
  128. package/onecode/cli.py +186 -6
  129. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  130. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  131. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  132. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  133. package/onecode/codebase/chunker.py +0 -5
  134. package/onecode/codebase/indexer.py +14 -1
  135. package/onecode/codebase/retriever.py +13 -2
  136. package/onecode/codebase/storage.py +5 -4
  137. package/onecode/commands.py +81 -6
  138. package/onecode/config.py +41 -12
  139. package/onecode/config_screen.py +7 -3
  140. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  141. package/onecode/mcp/manager.py +2 -3
  142. package/onecode/memory/__init__.py +7 -53
  143. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  144. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  145. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  146. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  147. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  148. package/onecode/memory/backend.py +13 -39
  149. package/onecode/memory/pyramid.py +2 -71
  150. package/onecode/memory/recall.py +38 -3
  151. package/onecode/migrate.py +122 -0
  152. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  153. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  154. package/onecode/models/messages.py +58 -0
  155. package/onecode/models/provider.py +20 -2
  156. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  157. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  158. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  159. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  160. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  161. package/onecode/models/providers/anthropic_provider.py +59 -12
  162. package/onecode/models/providers/glm_provider.py +40 -0
  163. package/onecode/models/providers/minimax_provider.py +3 -0
  164. package/onecode/models/providers/minimaxi_provider.py +3 -0
  165. package/onecode/models/providers/ollama_provider.py +3 -0
  166. package/onecode/server/app.py +1 -1
  167. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  168. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  169. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  170. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  171. package/onecode/skills/argument_substitution.py +0 -1
  172. package/onecode/skills/bootstrap.py +124 -0
  173. package/onecode/skills/loader.py +35 -53
  174. package/onecode/skills/manager.py +2 -2
  175. package/onecode/storage/activity.py +3 -4
  176. package/onecode/storage/session.py +3 -4
  177. package/onecode/tasks/manager.py +23 -10
  178. package/onecode/trace/tracer.py +2 -3
  179. package/package.json +1 -1
  180. package/pyproject.toml +5 -1
  181. package/run.js +68 -14
  182. package/tui/__pycache__/about.cpython-314.pyc +0 -0
  183. package/tui/__pycache__/app.cpython-313.pyc +0 -0
  184. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  185. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  186. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  187. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  188. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  189. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  190. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  191. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  192. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  193. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  194. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  195. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  196. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  197. package/tui/acp/agent.py +125 -58
  198. package/tui/acp/api.py +12 -0
  199. package/tui/acp/messages.py +3 -1
  200. package/tui/acp/prompt.py +0 -57
  201. package/tui/acp/protocol.py +6 -0
  202. package/tui/app.py +11 -33
  203. package/tui/messages.py +2 -0
  204. package/tui/path_complete.py +1 -1
  205. package/tui/paths.py +16 -0
  206. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  207. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  208. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  209. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  210. package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
  211. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  212. package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
  213. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  214. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  215. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  216. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  217. package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
  218. package/tui/screens/component_picker.py +35 -15
  219. package/tui/screens/diff_screen.py +84 -0
  220. package/tui/screens/log.py +25 -25
  221. package/tui/screens/main.py +48 -26
  222. package/tui/screens/projects.tcss +5 -1
  223. package/tui/screens/projects_app.py +6 -6
  224. package/tui/screens/projects_screen.py +19 -39
  225. package/tui/screens/sessions.py +3 -5
  226. package/tui/session_tracker.py +3 -0
  227. package/tui/settings_schema.py +1 -1
  228. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  229. package/tui/visuals/columns.py +1 -2
  230. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  231. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  232. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  233. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  234. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  235. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  236. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  237. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  238. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  239. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  240. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  241. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  242. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  243. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  244. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  245. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  246. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  247. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  248. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  249. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  250. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  251. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  252. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  253. package/tui/widgets/agent_response.py +0 -1
  254. package/tui/widgets/agent_thought.py +0 -1
  255. package/tui/widgets/conversation.py +157 -14
  256. package/tui/widgets/diff_view.py +10 -0
  257. package/tui/widgets/load_more.py +24 -0
  258. package/tui/widgets/modified_files.py +86 -12
  259. package/tui/widgets/plan.py +1 -1
  260. package/tui/widgets/project_grid_select.py +9 -2
  261. package/tui/widgets/session_grid_select.py +6 -0
  262. package/tui/widgets/side_bar.py +10 -0
  263. package/tui/widgets/subagent.py +92 -58
  264. package/tui/widgets/subagent_screen.py +638 -115
  265. package/tui/widgets/terminal.py +1 -2
  266. package/tui/widgets/terminal_tool.py +11 -2
  267. package/tui/widgets/tool_call.py +35 -2
  268. package/onecode/memory/offload.py +0 -114
  269. package/onecode/memory/symbolic.py +0 -164
package/onecode/config.py CHANGED
@@ -1,13 +1,16 @@
1
+ import logging
1
2
  import os
2
3
  from dataclasses import dataclass, field
3
4
  from pathlib import Path
4
- from typing import Any, Optional
5
+ from typing import Optional
5
6
 
6
7
  import yaml
7
8
 
9
+ logger = logging.getLogger("onecode.config")
8
10
 
9
- CLOUD_DEV_HARNESS_DIR = Path.home() / ".cdh"
10
- GLOBAL_CONFIG_PATH = CLOUD_DEV_HARNESS_DIR / "onecode.config.yaml"
11
+
12
+ ONECODE_DIR = Path.home() / ".onecode"
13
+ GLOBAL_CONFIG_PATH = ONECODE_DIR / "onecode.config.yaml"
11
14
 
12
15
 
13
16
  @dataclass
@@ -22,7 +25,7 @@ class ObservabilityConfig:
22
25
  trace_enabled: bool = True
23
26
  trace_exporter: str = "file"
24
27
  otlp_endpoint: str = "http://localhost:4317"
25
- trace_dir: str = "~/.cdh/traces"
28
+ trace_dir: str = "~/.onecode/traces"
26
29
 
27
30
 
28
31
  @dataclass
@@ -86,6 +89,13 @@ class CodebaseConfig:
86
89
  ])
87
90
 
88
91
 
92
+ @dataclass
93
+ class MemoryConfig:
94
+ enabled: bool = True
95
+ auto_recall: bool = True
96
+ top_k: int = 5
97
+
98
+
89
99
  @dataclass
90
100
  class GlobalConfig:
91
101
  default_mode: str = "build"
@@ -102,6 +112,7 @@ class GlobalConfig:
102
112
  log_level: str = "info"
103
113
  session_auto_save: bool = True
104
114
  codebase: CodebaseConfig = field(default_factory=CodebaseConfig)
115
+ memory: MemoryConfig = field(default_factory=MemoryConfig)
105
116
 
106
117
 
107
118
  def _dict_to_dataclass(cls, data: dict):
@@ -133,22 +144,40 @@ def _dict_to_dataclass(cls, data: dict):
133
144
 
134
145
  def ensure_dirs():
135
146
  dirs = [
136
- CLOUD_DEV_HARNESS_DIR,
137
- CLOUD_DEV_HARNESS_DIR / "sessions",
138
- CLOUD_DEV_HARNESS_DIR / "skills",
139
- CLOUD_DEV_HARNESS_DIR / "mcps",
140
- CLOUD_DEV_HARNESS_DIR / "traces",
141
- CLOUD_DEV_HARNESS_DIR / "logs",
142
- CLOUD_DEV_HARNESS_DIR / "models",
147
+ ONECODE_DIR,
148
+ ONECODE_DIR / "sessions",
149
+ ONECODE_DIR / "skills",
150
+ ONECODE_DIR / "mcps",
151
+ ONECODE_DIR / "traces",
152
+ ONECODE_DIR / "logs",
153
+ ONECODE_DIR / "models",
154
+ ONECODE_DIR / "codebase" / "indexes",
155
+ ONECODE_DIR / "memory",
143
156
  ]
144
157
  for d in dirs:
145
158
  d.mkdir(parents=True, exist_ok=True)
146
159
 
160
+ # One-time migration from legacy ~/.cdh/ to ~/.onecode/
161
+ try:
162
+ from onecode.migrate import migrate_legacy_cdh_to_onecode
163
+ result = migrate_legacy_cdh_to_onecode()
164
+ if result:
165
+ logger.warning("Legacy migration: %s", result)
166
+ except Exception as exc:
167
+ logger.warning("Legacy migration failed: %s", exc)
168
+
147
169
 
148
170
  def load_config() -> GlobalConfig:
149
171
  ensure_dirs()
150
172
  if not GLOBAL_CONFIG_PATH.exists():
151
173
  _write_default_config()
174
+ # Bootstrap ai-dlc-skill from source into cdh platform pool
175
+ try:
176
+ from onecode.skills.bootstrap import ensure_ai_dlc_skill, ensure_onecode_default_skills
177
+ ensure_onecode_default_skills()
178
+ ensure_ai_dlc_skill()
179
+ except Exception:
180
+ pass
152
181
  raw = yaml.safe_load(GLOBAL_CONFIG_PATH.read_text()) or {}
153
182
  return _dict_to_dataclass(GlobalConfig, raw)
154
183
 
@@ -201,7 +230,7 @@ def _write_default_config():
201
230
  "trace_enabled": True,
202
231
  "trace_exporter": "file",
203
232
  "otlp_endpoint": "http://localhost:4317",
204
- "trace_dir": "~/.cdh/traces",
233
+ "trace_dir": "~/.onecode/traces",
205
234
  },
206
235
  "attachments": {
207
236
  "max_size_mb": 10,
@@ -7,7 +7,7 @@ from textual.containers import Horizontal, Vertical
7
7
  from textual.reactive import var
8
8
  from textual.screen import ModalScreen
9
9
  from textual.widget import Widget
10
- from textual.widgets import Button, Input, Label, Select, Static
10
+ from textual.widgets import Button, Input, Label, Static
11
11
 
12
12
  from onecode.config import GlobalConfig, load_config, resolve_env, save_config
13
13
 
@@ -143,7 +143,7 @@ class EditFieldScreen(ModalScreen[str]):
143
143
  EditFieldScreen { background: rgba(0,0,0,0.7); align: center middle; }
144
144
  #edit-dialog { width: 50; height: 7; background: #111; border: solid #555; }
145
145
  #edit-label { height: 1; background: #333; color: #fff; padding: 0 1; }
146
- #edit-input { height: 3; padding: 0 1; }
146
+ #edit-input { height: 3; padding: 0 1; background: #111; color: #fff; }
147
147
  #edit-buttons { height: 3; background: #222; align: center middle; }
148
148
  Button { margin: 0 1; background: #444; color: #fff; }
149
149
  Button:hover { background: #666; }
@@ -158,6 +158,11 @@ class EditFieldScreen(ModalScreen[str]):
158
158
  yield Button("Cancel", id="edit-cancel")
159
159
  yield Button("Save", id="edit-save")
160
160
 
161
+ def on_mount(self) -> None:
162
+ inp = self.query_one("#edit-input", Input)
163
+ inp.selection_color = "#000"
164
+ inp.selection_background = "#fff"
165
+
161
166
  @on(Button.Pressed, "#edit-save")
162
167
  def on_save(self) -> None:
163
168
  value = self.query_one("#edit-input", Input).value.strip()
@@ -333,7 +338,6 @@ class ConfigScreen(App):
333
338
  self._section_fields.append(ConfigItem("log_level", "Log", cfg.log_level))
334
339
  case "providers":
335
340
  for pid, pcfg in cfg.providers.items():
336
- n_models = len(pcfg.models)
337
341
  ak = _truncate(_display_val(pcfg.api_key), 24)
338
342
  ep = _truncate(_display_val(pcfg.endpoint), 20)
339
343
  status = f"ep:{ep} ak:{ak}"
@@ -3,13 +3,12 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import json
5
5
  import logging
6
- from pathlib import Path
7
6
  from typing import Any, Optional
8
7
 
9
8
  import httpx
10
9
  import yaml
11
10
 
12
- from onecode.config import CLOUD_DEV_HARNESS_DIR
11
+ from onecode.config import ONECODE_DIR
13
12
  from onecode.mcp.client import MCPClient, MCPTool
14
13
 
15
14
  logger = logging.getLogger("onecode.mcp")
@@ -83,7 +82,7 @@ class MCPSSEClient:
83
82
 
84
83
  class MCPManager:
85
84
  def __init__(self):
86
- self.config_dir = CLOUD_DEV_HARNESS_DIR / "mcps"
85
+ self.config_dir = ONECODE_DIR / "mcps"
87
86
  self.config_dir.mkdir(parents=True, exist_ok=True)
88
87
  self.config_path = self.config_dir / "mcps.yaml"
89
88
  self._data: dict = {}
@@ -1,18 +1,9 @@
1
1
  """
2
- CDH Memory - Layered Long-term Memory for AI Agents
2
+ CDH Memory - Long-term Conversation Memory for AI Agents
3
3
 
4
- Based on TencentDB-Agent-Memory architecture:
5
- - L0: Raw conversation
6
- - L1: Atomic facts
7
- - L2: Scenarios
8
- - L3: Personas
9
-
10
- With symbolic short-term memory (Mermaid canvas + context offloading)
4
+ L0: Raw conversation storage with BM25 keyword recall.
11
5
  """
12
-
13
6
  from onecode.memory.pyramid import MemoryPyramid, MemoryLayer, MemoryEntry
14
- from onecode.memory.symbolic import SymbolicMemory, MermaidCanvas, NodeType
15
- from onecode.memory.offload import ContextOffloader, OffloadConfig, ToolCallExtractor
16
7
  from onecode.memory.recall import HybridRecall, BM25, RecallResult
17
8
  from onecode.memory.backend import MemoryBackend
18
9
 
@@ -20,32 +11,25 @@ __all__ = [
20
11
  "MemoryPyramid",
21
12
  "MemoryLayer",
22
13
  "MemoryEntry",
23
- "SymbolicMemory",
24
- "MermaidCanvas",
25
- "NodeType",
26
- "ContextOffloader",
27
- "OffloadConfig",
28
- "ToolCallExtractor",
29
14
  "HybridRecall",
30
15
  "BM25",
31
16
  "RecallResult",
32
17
  "MemoryBackend",
18
+ "AgentMemory",
33
19
  ]
34
20
 
35
21
 
36
22
  class AgentMemory:
37
23
  """
38
- Unified memory interface combining layered long-term memory
39
- with symbolic short-term memory and hybrid recall.
24
+ Unified memory interface combining persistent L0 conversation storage
25
+ with BM25 keyword recall for long-term context.
40
26
  """
41
27
 
42
28
  def __init__(self, storage_path=None):
43
- from onecode.config import CLOUD_DEV_HARNESS_DIR
44
- self.storage_path = storage_path or (CLOUD_DEV_HARNESS_DIR / "memory")
29
+ from onecode.config import ONECODE_DIR
30
+ self.storage_path = storage_path or (ONECODE_DIR / "memory")
45
31
  self.backend = MemoryBackend(self.storage_path / "memory.db")
46
32
  self.pyramid = MemoryPyramid(self.storage_path)
47
- self.symbolic = SymbolicMemory()
48
- self.offloader = ContextOffloader(self.storage_path / "refs")
49
33
  self.recall = HybridRecall()
50
34
 
51
35
  def remember(self, layer: MemoryLayer, content: str, metadata: dict = None, parent_id: str = None) -> MemoryEntry:
@@ -63,33 +47,3 @@ class AgentMemory:
63
47
 
64
48
  def search_memories(self, query: str, top_k: int = 5) -> list[RecallResult]:
65
49
  return self.recall.hybrid_recall(query, top_k)
66
-
67
- def remember_conversation(self, conversation_id: str, messages: list[dict]) -> MemoryEntry:
68
- content = "\n".join(f"{m.get('role', 'user')}: {m.get('content', '')}" for m in messages)
69
- entry = self.remember(MemoryLayer.L0_CONVERSATION, content, {"conversation_id": conversation_id})
70
- return entry
71
-
72
- def extract_atoms(self, conversation_id: str, max_atoms: int = 20) -> list[MemoryEntry]:
73
- return self.pyramid.extract_atoms_from_conversation(conversation_id, max_atoms)
74
-
75
- def build_scenario(self, name: str, atom_ids: list[str]) -> MemoryEntry:
76
- scenario = self.pyramid.build_scenario_from_atoms(atom_ids, name)
77
- self.backend.add_entry(scenario.id, MemoryLayer.L2_SCENARIO.value, scenario.content, scenario.metadata)
78
- return scenario
79
-
80
- def build_persona(self, name: str, scenario_ids: list[str]) -> MemoryEntry:
81
- persona = self.pyramid.build_persona_from_scenarios(scenario_ids, name)
82
- self.backend.add_entry(persona.id, MemoryLayer.L3_PERSONA.value, persona.content, persona.metadata)
83
- return persona
84
-
85
- def offload_and_symbolize(self, node_id: str, verbose_content: str, task_label: str) -> str:
86
- ref_path = self.symbolic.offload_log(node_id, verbose_content)
87
- self.backend.save_ref(node_id, verbose_content)
88
- self.symbolic.create_result_node(task_label, verbose_content)
89
- return ref_path
90
-
91
- def get_canvas(self) -> str:
92
- return self.symbolic.get_canvas_markdown()
93
-
94
- def drill_down(self, entry_id: str) -> list[MemoryEntry]:
95
- return self.pyramid.drill_down(entry_id)
@@ -1,9 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
- import uuid
5
3
  from contextlib import contextmanager
6
- from dataclasses import dataclass
7
4
  from datetime import datetime, timezone
8
5
  from pathlib import Path
9
6
  from typing import Optional
@@ -11,7 +8,7 @@ from typing import Optional
11
8
  from sqlalchemy import Column, String, Text, DateTime, JSON, create_engine, Index
12
9
  from sqlalchemy.orm import declarative_base, sessionmaker, Session
13
10
 
14
- from onecode.config import CLOUD_DEV_HARNESS_DIR
11
+ from onecode.config import ONECODE_DIR
15
12
 
16
13
 
17
14
  Base = declarative_base()
@@ -34,20 +31,10 @@ class MemoryRecord(Base):
34
31
  )
35
32
 
36
33
 
37
- class RefRecord(Base):
38
- __tablename__ = "ref_records"
39
-
40
- id = Column(String(16), primary_key=True)
41
- node_id = Column(String(16), nullable=False, index=True)
42
- content = Column(Text, nullable=False)
43
- file_path = Column(String(512), nullable=True)
44
- created_at = Column(DateTime, nullable=False)
45
-
46
-
47
34
  class MemoryBackend:
48
35
  def __init__(self, db_path: Optional[Path] = None):
49
36
  if db_path is None:
50
- db_path = CLOUD_DEV_HARNESS_DIR / "memory" / "memory.db"
37
+ db_path = ONECODE_DIR / "memory" / "memory.db"
51
38
  db_path.parent.mkdir(parents=True, exist_ok=True)
52
39
  self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
53
40
  Base.metadata.create_all(self.engine)
@@ -87,12 +74,20 @@ class MemoryBackend:
87
74
  s.merge(record)
88
75
  return True
89
76
 
90
- def get_entry(self, entry_id: str) -> Optional[MemoryRecord]:
77
+ def get_entry(self, entry_id: str) -> Optional[dict]:
91
78
  with self.session() as s:
92
79
  record = s.query(MemoryRecord).filter_by(id=entry_id).first()
93
80
  if record:
94
- s.refresh(record)
95
- return record
81
+ return {
82
+ "id": record.id,
83
+ "layer": record.layer,
84
+ "content": record.content,
85
+ "timestamp": record.timestamp.isoformat() if record.timestamp else "",
86
+ "metadata_json": record.metadata_json,
87
+ "parent_id": record.parent_id,
88
+ "result_ref": record.result_ref,
89
+ }
90
+ return None
96
91
 
97
92
  def get_entries_by_layer(self, layer: str, limit: int = 100) -> list[MemoryRecord]:
98
93
  with self.session() as s:
@@ -103,8 +98,6 @@ class MemoryBackend:
103
98
  .limit(limit)
104
99
  .all()
105
100
  )
106
- for r in records:
107
- s.refresh(r)
108
101
  return records
109
102
 
110
103
  def get_entries_by_parent(self, parent_id: str) -> list[MemoryRecord]:
@@ -123,23 +116,6 @@ class MemoryBackend:
123
116
  query = query.filter_by(layer=layer)
124
117
  return query.limit(limit).all()
125
118
 
126
- def save_ref(self, node_id: str, content: str, file_path: Optional[str] = None) -> bool:
127
- with self.session() as s:
128
- record = RefRecord(
129
- id=str(uuid.uuid4())[:16],
130
- node_id=node_id,
131
- content=content,
132
- file_path=file_path,
133
- created_at=datetime.now(timezone.utc),
134
- )
135
- s.merge(record)
136
- return True
137
-
138
- def get_ref(self, node_id: str) -> Optional[str]:
139
- with self.session() as s:
140
- record = s.query(RefRecord).filter_by(node_id=node_id).order_by(RefRecord.created_at.desc()).first()
141
- return record.content if record else None
142
-
143
119
  def search_content(self, query: str, layer: Optional[str] = None, limit: int = 10) -> list[MemoryRecord]:
144
120
  with self.session() as s:
145
121
  q = s.query(MemoryRecord)
@@ -149,8 +125,6 @@ class MemoryBackend:
149
125
  pattern = f"%{escaped}%"
150
126
  q = q.filter(MemoryRecord.content.like(pattern, escape="\\"))
151
127
  records = q.order_by(MemoryRecord.timestamp.desc()).limit(limit).all()
152
- for r in records:
153
- s.refresh(r)
154
128
  return records
155
129
 
156
130
  def delete_entry(self, entry_id: str) -> bool:
@@ -11,9 +11,6 @@ from typing import Optional
11
11
 
12
12
  class MemoryLayer(Enum):
13
13
  L0_CONVERSATION = "l0_conversation"
14
- L1_ATOM = "l1_atom"
15
- L2_SCENARIO = "l2_scenario"
16
- L3_PERSONA = "l3_persona"
17
14
 
18
15
 
19
16
  @dataclass
@@ -69,8 +66,8 @@ class MemoryEntry:
69
66
 
70
67
  class MemoryPyramid:
71
68
  def __init__(self, storage_path: Optional[Path] = None):
72
- from onecode.config import CLOUD_DEV_HARNESS_DIR
73
- self.storage_path = storage_path or (CLOUD_DEV_HARNESS_DIR / "memory")
69
+ from onecode.config import ONECODE_DIR
70
+ self.storage_path = storage_path or (ONECODE_DIR / "memory")
74
71
  self.storage_path.mkdir(parents=True, exist_ok=True)
75
72
  self._layers = {layer: [] for layer in MemoryLayer}
76
73
  self._load_index()
@@ -134,69 +131,3 @@ class MemoryPyramid:
134
131
  def list_recent(self, layer: MemoryLayer, limit: int = 10) -> list[MemoryEntry]:
135
132
  entries = self._layers.get(layer, [])
136
133
  return sorted(entries, key=lambda e: e.timestamp, reverse=True)[:limit]
137
-
138
- def drill_down(self, entry_id: str) -> list[MemoryEntry]:
139
- chain = []
140
- current = self._find_entry_anywhere(entry_id)
141
- while current:
142
- chain.append(current)
143
- if current.parent_id:
144
- current = self._find_entry_anywhere(current.parent_id)
145
- else:
146
- current = None
147
- return list(reversed(chain))
148
-
149
- def _find_entry_anywhere(self, entry_id: str) -> Optional[MemoryEntry]:
150
- for entries in self._layers.values():
151
- for entry in entries:
152
- if entry.id == entry_id:
153
- return entry
154
- return None
155
-
156
- def extract_atoms_from_conversation(
157
- self, conversation_id: str, max_atoms: int = 20
158
- ) -> list[MemoryEntry]:
159
- l0_entries = [e for e in self._layers[MemoryLayer.L0_CONVERSATION] if e.parent_id == conversation_id]
160
- atoms = []
161
- for l0 in l0_entries:
162
- content = self.get_content(l0)
163
- if len(content) > 100:
164
- atom_content = content[:200] + "..."
165
- else:
166
- atom_content = content
167
- atom = self.add(
168
- MemoryLayer.L1_ATOM,
169
- atom_content,
170
- metadata={"source": "auto_extract", "source_id": l0.id},
171
- parent_id=l0.id,
172
- )
173
- atoms.append(atom)
174
- if len(atoms) >= max_atoms:
175
- break
176
- return atoms
177
-
178
- def build_scenario_from_atoms(self, atom_ids: list[str], name: str) -> MemoryEntry:
179
- atom_contents = []
180
- for atom_id in atom_ids:
181
- atom = self._find_entry_anywhere(atom_id)
182
- if atom:
183
- atom_contents.append(f"- {atom.content}")
184
- content = f"## {name}\n\n" + "\n".join(atom_contents)
185
- return self.add(
186
- MemoryLayer.L2_SCENARIO,
187
- content,
188
- metadata={"atom_count": len(atom_ids)},
189
- )
190
-
191
- def build_persona_from_scenarios(self, scenario_ids: list[str], name: str) -> MemoryEntry:
192
- scenario_contents = []
193
- for sc_id in scenario_ids:
194
- sc = self._find_entry_anywhere(sc_id)
195
- if sc:
196
- scenario_contents.append(f"### {sc.content[:100]}...\n{sc.content}")
197
- content = f"# {name}\n\n" + "\n".join(scenario_contents)
198
- return self.add(
199
- MemoryLayer.L3_PERSONA,
200
- content,
201
- metadata={"scenario_count": len(scenario_ids)},
202
- )
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import math
4
4
  import re
5
- from collections import Counter
6
5
  from dataclasses import dataclass
7
6
  from typing import Optional
8
7
 
@@ -17,6 +16,16 @@ class RecallResult:
17
16
 
18
17
 
19
18
  class BM25:
19
+ # Hard cap above which ``index()`` refuses to build the inverted index.
20
+ # The original O(V × N) construction of ``doc_freqs`` (set membership
21
+ # over Python lists) becomes effectively uncallable above a few
22
+ # thousand docs — a 79k-chunk corpus (observed in production where
23
+ # sessions kept re-indexing without cleaning the SQLite store) took
24
+ # ~1 hour, blocking the event loop and freezing streaming + cancel.
25
+ # Above this cap we degrade to a no-op index so callers fall back to
26
+ # other retrievers / explicit search instead of hanging the process.
27
+ _MAX_DOCS = 5000
28
+
20
29
  def __init__(self, k1: float = 1.5, b: float = 0.75):
21
30
  self.k1 = k1
22
31
  self.b = b
@@ -25,17 +34,43 @@ class BM25:
25
34
  self.doc_freqs: dict[str, int] = {}
26
35
  self.num_docs: int = 0
27
36
  self.doc_tokens: list[list[str]] = []
37
+ # Tokenize once per doc as ``set`` (not list) so the
38
+ # ``token in doc`` membership check inside the doc_freqs
39
+ # comprehension is O(1) instead of O(doc_len). This alone takes
40
+ # the 79k-doc construction from ~hours to ~seconds; the hard cap
41
+ # above is a safety net for pathological corpora.
42
+ self._doc_token_sets: list[set[str]] = []
28
43
 
29
44
  def index(self, documents: list[str]) -> None:
45
+ if len(documents) > self._MAX_DOCS:
46
+ import logging
47
+ logging.getLogger(__name__).warning(
48
+ "BM25.index refused: %d docs exceeds cap=%d (would block "
49
+ "the event loop for minutes); BM25 search will return no "
50
+ "results. Clean the codebase index or use a smaller "
51
+ "project scope.", len(documents), self._MAX_DOCS,
52
+ )
53
+ self.doc_tokens = []
54
+ self._doc_token_sets = []
55
+ self.doc_lengths = []
56
+ self.avgdl = 0
57
+ self.num_docs = 0
58
+ self.doc_freqs = {}
59
+ return
60
+
30
61
  self.doc_tokens = [self._tokenize(doc) for doc in documents]
62
+ self._doc_token_sets = [set(t) for t in self.doc_tokens]
31
63
  self.doc_lengths = [len(tokens) for tokens in self.doc_tokens]
32
64
  self.avgdl = sum(self.doc_lengths) / len(self.doc_lengths) if self.doc_lengths else 0
33
65
  self.num_docs = len(documents)
34
66
 
35
67
  all_tokens = set()
36
- for tokens in self.doc_tokens:
68
+ for tokens in self._doc_token_sets:
37
69
  all_tokens.update(tokens)
38
- self.doc_freqs = {token: sum(1 for doc in self.doc_tokens if token in doc) for token in all_tokens}
70
+ self.doc_freqs = {
71
+ token: sum(1 for ts in self._doc_token_sets if token in ts)
72
+ for token in all_tokens
73
+ }
39
74
 
40
75
  def _tokenize(self, text: str) -> list[str]:
41
76
  text = text.lower()
@@ -0,0 +1,122 @@
1
+ """onecode legacy migration — move onecode's private state from ~/.cdh/ to ~/.onecode/.
2
+
3
+ This is a ONE-TIME migration triggered at first startup after upgrade.
4
+ It only moves directories owned by onecode (logs, traces, memory,
5
+ snapshots, mcps, models, onecode.config.yaml). cdh platform directories
6
+ (skills, projects, state, sessions) are left untouched. Session JSON is owned
7
+ by the cdh platform layer (B mapping mode), not by individual engines.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import shutil
15
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger("onecode.migrate")
18
+
19
+ # Directories/state that belong to onecode and should be migrated
20
+ # NOTE: sessions is deliberately excluded — it belongs to the cdh
21
+ # platform layer (B mapping mode), not to any individual engine.
22
+ _ONECODE_PRIVATE_DIRS = {
23
+ "logs",
24
+ "traces",
25
+ "memory",
26
+ "snapshots",
27
+ "mcps",
28
+ "models",
29
+ }
30
+
31
+ # cdh platform directories — NEVER touch
32
+ _CDH_PLATFORM_DIRS = {"skills", "projects", "state"}
33
+
34
+ # Dotfiles that belong to onecode (at the ~/.cdh/ root level)
35
+ _ONECODE_DOTFILES = {"onecode.config.yaml"}
36
+
37
+
38
+ def migrate_legacy_cdh_to_onecode(
39
+ legacy_dir: Path | None = None,
40
+ target_dir: Path | None = None,
41
+ ) -> str | None:
42
+ """Migrate onecode's private state from ~/.cdh/ to ~/.onecode/.
43
+
44
+ Args:
45
+ legacy_dir: Source directory (default ~/.cdh/).
46
+ target_dir: Target directory (default ~/.onecode/).
47
+
48
+ Returns:
49
+ Info/warning message string, or None if no migration needed.
50
+ """
51
+ legacy = (legacy_dir or Path.home() / ".cdh").resolve()
52
+ target = (target_dir or Path.home() / ".onecode").resolve()
53
+
54
+ # Nothing to migrate
55
+ if not legacy.exists():
56
+ return None
57
+
58
+ # Already migrated (marker exists)
59
+ migrated_marker = target / ".migrated_from"
60
+ if migrated_marker.exists():
61
+ return None
62
+
63
+ # Both exist but no marker → user may have manually set up ~/.onecode/
64
+ if target.exists():
65
+ # Check if target has any onecode dirs already
66
+ has_content = any(
67
+ (target / d).exists() for d in _ONECODE_PRIVATE_DIRS
68
+ ) or target / "onecode.config.yaml" in target.iterdir()
69
+ if has_content:
70
+ return None # 各自独立运行,无需迁移
71
+ # Target exists but empty — safe to proceed
72
+
73
+ # Perform migration
74
+ target.mkdir(parents=True, exist_ok=True)
75
+ migrated_items = []
76
+
77
+ # Migrate private subdirectories
78
+ for dir_name in _ONECODE_PRIVATE_DIRS:
79
+ src = legacy / dir_name
80
+ if src.exists() and src.is_dir():
81
+ dst = target / dir_name
82
+ if dst.exists():
83
+ # Merge contents (don't overwrite existing)
84
+ for item in src.iterdir():
85
+ dst_item = dst / item.name
86
+ if not dst_item.exists():
87
+ shutil.move(str(item), str(dst_item))
88
+ else:
89
+ shutil.move(str(src), str(dst))
90
+ migrated_items.append(dir_name)
91
+
92
+ # Migrate onecode dotfiles
93
+ for fname in _ONECODE_DOTFILES:
94
+ src = legacy / fname
95
+ if src.exists() and src.is_file():
96
+ dst = target / fname
97
+ if not dst.exists():
98
+ shutil.move(str(src), str(dst))
99
+ migrated_items.append(fname)
100
+
101
+ # Write migration marker
102
+ marker_data = {
103
+ "migrated_from": str(legacy),
104
+ "migrated_to": str(target),
105
+ "items": sorted(migrated_items),
106
+ "preserved_on_legacy": sorted(_CDH_PLATFORM_DIRS),
107
+ }
108
+ migrated_marker.write_text(
109
+ json.dumps(marker_data, ensure_ascii=False, indent=2),
110
+ encoding="utf-8",
111
+ )
112
+
113
+ if migrated_items:
114
+ msg = (
115
+ f"⚠ Migrated onecode state from {legacy} → {target}: "
116
+ f"{', '.join(migrated_items)}. "
117
+ f"cdh platform dirs ({', '.join(_CDH_PLATFORM_DIRS)}) preserved at {legacy}."
118
+ )
119
+ logger.warning(msg)
120
+ return msg
121
+
122
+ return None