memleaf 0.2.36__tar.gz → 0.2.38__tar.gz

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 (191) hide show
  1. {memleaf-0.2.36 → memleaf-0.2.38}/CHANGELOG.md +15 -0
  2. {memleaf-0.2.36/src/memleaf.egg-info → memleaf-0.2.38}/PKG-INFO +2 -2
  3. {memleaf-0.2.36 → memleaf-0.2.38}/README.en.md +1 -1
  4. {memleaf-0.2.36 → memleaf-0.2.38}/README.md +1 -1
  5. {memleaf-0.2.36 → memleaf-0.2.38}/pyproject.toml +4 -1
  6. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/config.py +21 -0
  8. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  9. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/hermes_runtime.py +35 -1
  10. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/installer.py +64 -5
  11. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/base.py +10 -0
  12. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/openai_compatible.py +36 -0
  13. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/router.py +17 -0
  14. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/memory_planner.py +55 -77
  15. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/model_execution.py +136 -12
  16. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/planning_context.py +1 -31
  17. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/process_jobs.py +83 -0
  18. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/prompts.py +84 -57
  19. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/update_review.py +8 -1
  20. {memleaf-0.2.36 → memleaf-0.2.38/src/memleaf.egg-info}/PKG-INFO +2 -2
  21. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf.egg-info/SOURCES.txt +3 -1
  22. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf.egg-info/entry_points.txt +3 -0
  23. memleaf-0.2.38/tests/test_gate_scope_latency_v038.py +201 -0
  24. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_runtime_install.py +35 -3
  25. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_maintenance_v2.py +2 -0
  26. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_new_scope_source_grounding.py +22 -6
  27. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_processing_observability_concurrency.py +6 -1
  28. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_pypi_install.py +5 -1
  29. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_session_lineage.py +8 -4
  30. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b1.py +12 -5
  31. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3d_scope_maintenance.py +3 -3
  32. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_c3_packaging.py +4 -0
  33. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v023_scope_correction.py +1 -1
  34. memleaf-0.2.38/tests/test_windows_public_mcp_launcher.py +75 -0
  35. {memleaf-0.2.36 → memleaf-0.2.38}/IMPLEMENTATION_PLAN.md +0 -0
  36. {memleaf-0.2.36 → memleaf-0.2.38}/LICENSE +0 -0
  37. {memleaf-0.2.36 → memleaf-0.2.38}/MANIFEST.in +0 -0
  38. {memleaf-0.2.36 → memleaf-0.2.38}/RELEASE_CHECKLIST.md +0 -0
  39. {memleaf-0.2.36 → memleaf-0.2.38}/docs/capture-budget-design.md +0 -0
  40. {memleaf-0.2.36 → memleaf-0.2.38}/docs/config-migrations.md +0 -0
  41. {memleaf-0.2.36 → memleaf-0.2.38}/docs/core-refactor.md +0 -0
  42. {memleaf-0.2.36 → memleaf-0.2.38}/docs/evidence-retention.md +0 -0
  43. {memleaf-0.2.36 → memleaf-0.2.38}/docs/gate-evidence-boundary.md +0 -0
  44. {memleaf-0.2.36 → memleaf-0.2.38}/docs/general-processing.md +0 -0
  45. {memleaf-0.2.36 → memleaf-0.2.38}/docs/hermes-mcp-runtime.md +0 -0
  46. {memleaf-0.2.36 → memleaf-0.2.38}/docs/performance.md +0 -0
  47. {memleaf-0.2.36 → memleaf-0.2.38}/docs/processing-quality-acceptance.md +0 -0
  48. {memleaf-0.2.36 → memleaf-0.2.38}/docs/v0.2.26-processing-status.md +0 -0
  49. {memleaf-0.2.36 → memleaf-0.2.38}/examples/README.md +0 -0
  50. {memleaf-0.2.36 → memleaf-0.2.38}/examples/basic_usage.py +0 -0
  51. {memleaf-0.2.36 → memleaf-0.2.38}/examples/live_core_lifecycle_acceptance.py +0 -0
  52. {memleaf-0.2.36 → memleaf-0.2.38}/examples/live_processing_acceptance.py +0 -0
  53. {memleaf-0.2.36 → memleaf-0.2.38}/examples/mcp_stdio.ndjson +0 -0
  54. {memleaf-0.2.36 → memleaf-0.2.38}/install.ps1 +0 -0
  55. {memleaf-0.2.36 → memleaf-0.2.38}/install.sh +0 -0
  56. {memleaf-0.2.36 → memleaf-0.2.38}/setup.cfg +0 -0
  57. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/__main__.py +0 -0
  58. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/adapters/__init__.py +0 -0
  59. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/adapters/antigravity.py +0 -0
  60. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/adapters/base.py +0 -0
  61. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/adapters/codex.py +0 -0
  62. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/adapters/hermes.py +0 -0
  63. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/admission.py +0 -0
  64. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/budget.py +0 -0
  65. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/capture.py +0 -0
  66. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/cli.py +0 -0
  67. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/compaction.py +0 -0
  68. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/create_coordinator.py +0 -0
  69. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/credentials.py +0 -0
  70. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/evidence_budget.py +0 -0
  71. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/evidence_policy.py +0 -0
  72. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/frontmatter.py +0 -0
  73. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/hermes_provider/README.md +0 -0
  74. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/hermes_provider/__init__.py +0 -0
  75. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
  76. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/host_events.py +0 -0
  77. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/host_runtime.py +0 -0
  78. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/inbox.py +0 -0
  79. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/index.py +0 -0
  80. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/inspection.py +0 -0
  81. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/__init__.py +0 -0
  82. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/claude_compatible.py +0 -0
  83. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/llm/gemini.py +0 -0
  84. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/locking.py +0 -0
  85. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/mcp_server.py +0 -0
  86. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/memory_commit.py +0 -0
  87. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/memory_writer.py +0 -0
  88. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/model_discovery.py +0 -0
  89. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/models.py +0 -0
  90. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/native_index.py +0 -0
  91. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/native_registration.py +0 -0
  92. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/parallel_model.py +0 -0
  93. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/process_common.py +0 -0
  94. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/process_journal.py +0 -0
  95. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/process_owner.py +0 -0
  96. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/processing.py +0 -0
  97. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/provenance.py +0 -0
  98. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/recording_policy.py +0 -0
  99. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/redaction.py +0 -0
  100. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/retention.py +0 -0
  101. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/retrieval.py +0 -0
  102. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/retrieval_gate.py +0 -0
  103. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/scope_maintenance.py +0 -0
  104. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/scope_state.py +0 -0
  105. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/service.py +0 -0
  106. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/source_policy.py +0 -0
  107. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/state_layout.py +0 -0
  108. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/target_reconciliation.py +0 -0
  109. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/turn_audit.py +0 -0
  110. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/turn_plan.py +0 -0
  111. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/update_coordinator.py +0 -0
  112. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/validation.py +0 -0
  113. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf/vault.py +0 -0
  114. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf.egg-info/dependency_links.txt +0 -0
  115. {memleaf-0.2.36 → memleaf-0.2.38}/src/memleaf.egg-info/top_level.txt +0 -0
  116. {memleaf-0.2.36 → memleaf-0.2.38}/tests/__init__.py +0 -0
  117. {memleaf-0.2.36 → memleaf-0.2.38}/tests/semantic_fixtures.py +0 -0
  118. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_admission_noise.py +0 -0
  119. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_automatic_duplicate_noop_collision.py +0 -0
  120. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_candidate_polarity.py +0 -0
  121. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_codex_install.py +0 -0
  122. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_codex_native_cli.py +0 -0
  123. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_config_migrations_v028.py +0 -0
  124. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_context_budget.py +0 -0
  125. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_conversation_only.py +0 -0
  126. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_credential_safety.py +0 -0
  127. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_cross_host_acceptance.py +0 -0
  128. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_cross_turn_dedupe_regressions.py +0 -0
  129. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_due_date_grounding.py +0 -0
  130. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_due_date_grounding_retry.py +0 -0
  131. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_email_actionable_coverage.py +0 -0
  132. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_evidence_budget.py +0 -0
  133. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_evidence_retention_policy.py +0 -0
  134. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_external_source_dates.py +0 -0
  135. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_extraction_quality_regressions.py +0 -0
  136. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_gate_capacity.py +0 -0
  137. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_gate_schema_repair.py +0 -0
  138. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_general_evidence_admission.py +0 -0
  139. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_general_tool_provenance.py +0 -0
  140. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_global_todo_acceptance.py +0 -0
  141. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_global_todo_query_no_write.py +0 -0
  142. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_global_todo_retrieval.py +0 -0
  143. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_native_registration.py +0 -0
  144. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_provider.py +0 -0
  145. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_stdio_transport.py +0 -0
  146. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_transport_evidence.py +0 -0
  147. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_hermes_windows_subprocess.py +0 -0
  148. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_host_events.py +0 -0
  149. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_host_runtime_contract.py +0 -0
  150. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_inspection_state_v028.py +0 -0
  151. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_install.py +0 -0
  152. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_long_run_hygiene.py +0 -0
  153. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_model_discovery.py +0 -0
  154. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_model_owned_fields.py +0 -0
  155. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_partial_retry_idempotency.py +0 -0
  156. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_phase2_model_decisions.py +0 -0
  157. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_process_jobs.py +0 -0
  158. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_process_owner_locking.py +0 -0
  159. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_processing_contract_v026.py +0 -0
  160. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_read_only_deferred_isolation.py +0 -0
  161. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_retrieval_gate.py +0 -0
  162. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_retrieval_v2.py +0 -0
  163. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_review_source_context.py +0 -0
  164. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_revision_digest.py +0 -0
  165. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_shared_memory_refactor.py +0 -0
  166. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_source_neutral_todos_v028.py +0 -0
  167. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_a.py +0 -0
  168. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b2a.py +0 -0
  169. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b2b.py +0 -0
  170. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3a_commit.py +0 -0
  171. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3a_contract.py +0 -0
  172. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3b_native_context.py +0 -0
  173. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3b_native_index.py +0 -0
  174. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3b_scope.py +0 -0
  175. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_b3c_retrieval.py +0 -0
  176. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_c1_mcp.py +0 -0
  177. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_stage_c2_init.py +0 -0
  178. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_state_layout_v028.py +0 -0
  179. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_summary_date_grounding_integration.py +0 -0
  180. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_target_reconciliation.py +0 -0
  181. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_target_reconciliation_integration.py +0 -0
  182. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_todo_state_recovery.py +0 -0
  183. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_update_review.py +0 -0
  184. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_update_target_recovery.py +0 -0
  185. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_upgrade_preserves_vault.py +0 -0
  186. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v2_gate_limits.py +0 -0
  187. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v2_host_flow.py +0 -0
  188. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v2_mcp_flow.py +0 -0
  189. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v2_nomatch_semantics.py +0 -0
  190. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_v2_search_gate_acceptance.py +0 -0
  191. {memleaf-0.2.36 → memleaf-0.2.38}/tests/test_whole_unit_bindings.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.38 — 2026-09-09
6
+
7
+ - Unify automatic project-Scope grounding: registered and newly named model-selected projects now use the same exact candidate-bound source check. Remove the later registered-name occurrence conflict scan that could misclassify an implementation platform/product mention as ownership and reject the correct new project.
8
+ - Strengthen final CREATE/UPDATE semantic review so a `project:<name>` Scope with `scope_source=model` is itself treated as an affiliation claim; product/platform/system/notification/implementation mentions cannot authorize project ownership, and an explicit contradictory owner defers instead of silently changing Scope.
9
+ - Add safe per-model-call telemetry with fixed operation classes (`gate_primary`, `gate_coverage_repair`, format repair, summarize/review/coordination variants), request duration, input/output size, provider token usage, DeepSeek cache-hit/miss tokens and reasoning-token counts when supplied. Prompt/response text and credentials are never persisted.
10
+ - Add explicit `llm.thinking` configuration for Gate/summarize/compact. The default is `low`, retaining reasoning at the lowest supported effort; users may select `disabled`, `default`, `high`, or `max` explicitly. DeepSeek OpenAI-format calls send the corresponding thinking controls.
11
+ - Reduce Gate input cost by removing a duplicated system-policy tail and replace coverage re-checks with a narrow unresolved-evidence protocol instead of rerunning the full Gate prompt. Deterministic validation does not claim a specific real-provider latency reduction.
12
+
13
+ ## 0.2.37 — 2026-09-09
14
+
15
+ - Add a dedicated `memleaf-mcpw` GUI entry point for the Hermes public MCP on Windows. The GUI-subsystem launcher does not allocate a console window even when an older Hermes/MCP SDK starts it without `CREATE_NO_WINDOW`; it enters the same `memleaf.mcp_server:main` implementation and keeps the same stdio JSON-RPC protocol.
16
+ - Keep the Hermes MemoryProvider private MCP on `memleaf-mcp.exe` with the v0.2.36 `CREATE_NO_WINDOW` protection, so both Windows launch paths are covered without changing macOS/Linux behavior or the Markdown Vault architecture.
17
+ - Treat `memleaf-mcp.exe` and its sibling `memleaf-mcpw.exe` as the same installed memleaf runtime for preflight/runtime policy while still requiring the GUI launcher for the public Hermes MCP. Existing v0.2.36 direct entries in the same runtime migrate automatically; different-runtime and different-Vault protections remain fail closed.
18
+ - Add Windows acceptance that inspects the installed launcher PE Subsystem, starts `memleaf-mcpw.exe` with `creationflags=0`, performs real MCP initialize and tools/list over redirected stdio, confirms all 13 tools, and verifies clean EOF shutdown.
19
+
5
20
  ## 0.2.36 — 2026-09-08
6
21
 
7
22
  - Prevent the Hermes MemoryProvider from opening a visible console/Windows Terminal window whenever it starts its private `memleaf-mcp` stdio child on Windows. Provider-owned MCP launches now use `subprocess.CREATE_NO_WINDOW`; stdin/stdout pipes, stderr suppression, timeout handling, process reuse and shutdown semantics are unchanged.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.36
3
+ Version: 0.2.38
4
4
  Summary: A local-first Markdown memory core for AI agents
5
5
  Author: memleaf contributors
6
6
  License-Expression: MIT
@@ -23,7 +23,7 @@ Dynamic: license-file
23
23
 
24
24
  [English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
25
25
 
26
- > **版本:0.2.36。**
26
+ > **版本:0.2.38。**
27
27
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
6
6
 
7
- > **Version: 0.2.36.**
7
+ > **Version: 0.2.38.**
8
8
  > Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
9
9
  > **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
6
6
 
7
- > **版本:0.2.36。**
7
+ > **版本:0.2.38。**
8
8
  > 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.36"
7
+ version = "0.2.38"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -27,6 +27,9 @@ dependencies = []
27
27
  memleaf-mcp = "memleaf.mcp_server:main"
28
28
  memleaf = "memleaf.cli:main"
29
29
 
30
+ [project.gui-scripts]
31
+ memleaf-mcpw = "memleaf.mcp_server:main"
32
+
30
33
  [tool.setuptools.packages.find]
31
34
  where = ["src"]
32
35
 
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.36"
3
+ __version__ = "0.2.38"
4
4
 
5
5
  from .config import DEFAULT_CONFIG, default_config, load_config, save_config
6
6
  from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
@@ -19,6 +19,9 @@ MAX_REQUEST_TIMEOUT = 240
19
19
  DEFAULT_MODEL_CONCURRENCY = 3
20
20
  MIN_MODEL_CONCURRENCY = 1
21
21
  MAX_MODEL_CONCURRENCY = 8
22
+ THINKING_PURPOSES = ("gate", "summarize", "compact")
23
+ THINKING_MODES = frozenset({"default", "disabled", "low", "high", "max"})
24
+ DEFAULT_THINKING = {purpose: "low" for purpose in THINKING_PURPOSES}
22
25
 
23
26
 
24
27
  def _normalize_request_timeout(value: Any) -> int | float:
@@ -41,6 +44,21 @@ def _normalize_model_concurrency(value: Any) -> int:
41
44
  return value
42
45
 
43
46
 
47
+ def _normalize_thinking_settings(value: Any) -> dict[str, str]:
48
+ if value is None:
49
+ value = {}
50
+ if not isinstance(value, Mapping):
51
+ raise ValueError("invalid memleaf llm.thinking settings")
52
+ if set(value) - set(THINKING_PURPOSES):
53
+ raise ValueError("invalid memleaf llm.thinking settings")
54
+ result = dict(DEFAULT_THINKING)
55
+ for purpose, mode in value.items():
56
+ if not isinstance(mode, str) or mode not in THINKING_MODES:
57
+ raise ValueError("invalid memleaf llm.thinking settings")
58
+ result[purpose] = mode
59
+ return result
60
+
61
+
44
62
  DEFAULT_CONFIG: dict[str, Any] = {
45
63
  "vault": "~/.memleaf",
46
64
  "agents": {"codex": True, "hermes": True, "antigravity": False},
@@ -75,6 +93,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
75
93
  "context_window": 200000,
76
94
  "request_timeout": DEFAULT_REQUEST_TIMEOUT,
77
95
  "diagnostic_logging": False,
96
+ "thinking": dict(DEFAULT_THINKING),
78
97
  },
79
98
  }
80
99
 
@@ -188,6 +207,7 @@ def load_config(path: Path | str, *, vault: Path | str | None = None) -> dict[st
188
207
  raise ValueError("invalid memleaf llm settings")
189
208
  llm = dict(llm)
190
209
  llm["request_timeout"] = _normalize_request_timeout(llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT))
210
+ llm["thinking"] = _normalize_thinking_settings(llm.get("thinking"))
191
211
  if type(llm.get("diagnostic_logging", False)) is not bool:
192
212
  raise ValueError("invalid memleaf llm.diagnostic_logging")
193
213
  merged["llm"] = llm
@@ -231,6 +251,7 @@ def save_config(path: Path | str, config: Mapping[str, Any]) -> None:
231
251
  normalized_llm["request_timeout"] = _normalize_request_timeout(
232
252
  normalized_llm.get("request_timeout", DEFAULT_REQUEST_TIMEOUT)
233
253
  )
254
+ normalized_llm["thinking"] = _normalize_thinking_settings(normalized_llm.get("thinking"))
234
255
  diagnostic_logging = normalized_llm.get("diagnostic_logging", False)
235
256
  if type(diagnostic_logging) is not bool:
236
257
  raise ValueError("invalid memleaf llm.diagnostic_logging")
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.36
2
+ version: 0.2.38
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -18,7 +18,7 @@ from typing import Any, Mapping, Sequence
18
18
  from .adapters.hermes import _parse_json_or_yaml_mcp
19
19
 
20
20
 
21
- _MEMLEAF_COMMAND_NAMES = frozenset({"memleaf-mcp", "memleaf-mcp.exe"})
21
+ _MEMLEAF_COMMAND_NAMES = frozenset({"memleaf-mcp", "memleaf-mcp.exe", "memleaf-mcpw", "memleaf-mcpw.exe"})
22
22
  _TRUE_VALUES = frozenset({"true", "1", "yes", "on"})
23
23
  _FALSE_VALUES = frozenset({"false", "0", "no", "off"})
24
24
 
@@ -111,6 +111,27 @@ def is_absolute_memleaf_command(
111
111
  return Path(text).is_absolute()
112
112
 
113
113
 
114
+ def memleaf_commands_same_runtime(
115
+ left: Path | str,
116
+ right: Path | str,
117
+ *,
118
+ platform: str | None = None,
119
+ ) -> bool:
120
+ """Return whether two absolute memleaf launchers belong to one runtime."""
121
+
122
+ if host_paths_equivalent(left, right, platform=platform):
123
+ return True
124
+ if _platform_name(platform) != "nt":
125
+ return False
126
+ left_text = _path_text(left).replace("/", "\\")
127
+ right_text = _path_text(right).replace("/", "\\")
128
+ if not is_absolute_memleaf_command(left_text, platform="nt"):
129
+ return False
130
+ if not is_absolute_memleaf_command(right_text, platform="nt"):
131
+ return False
132
+ return ntpath.normcase(ntpath.dirname(left_text)) == ntpath.normcase(ntpath.dirname(right_text))
133
+
134
+
114
135
  def _is_bare_memleaf_command(command: str, *, platform: str | None = None) -> bool:
115
136
  text = command.strip()
116
137
  return (
@@ -172,6 +193,7 @@ def inspect_hermes_mcp(
172
193
  expected_command: Path | str,
173
194
  *,
174
195
  platform: str | None = None,
196
+ allow_same_runtime: bool = False,
175
197
  ) -> HermesMcpInspection:
176
198
  """Inspect ``mcp_servers.memleaf`` without executing configured commands.
177
199
 
@@ -313,6 +335,17 @@ def inspect_hermes_mcp(
313
335
  configured_command=command,
314
336
  configured_vault=configured_vault,
315
337
  )
338
+ if allow_same_runtime and memleaf_commands_same_runtime(
339
+ command, expected_command, platform=platform
340
+ ):
341
+ return _result(
342
+ "correct",
343
+ "the existing memleaf MCP entry uses a sibling launcher from this runtime and Vault",
344
+ config_path=path,
345
+ expected_command=expected_command,
346
+ configured_command=command,
347
+ configured_vault=configured_vault,
348
+ )
316
349
  if _is_bare_memleaf_command(command, platform=platform):
317
350
  return _result(
318
351
  "legacy",
@@ -346,4 +379,5 @@ __all__ = [
346
379
  "host_paths_equivalent",
347
380
  "inspect_hermes_mcp",
348
381
  "is_absolute_memleaf_command",
382
+ "memleaf_commands_same_runtime",
349
383
  ]
@@ -38,6 +38,7 @@ from .hermes_runtime import (
38
38
  HermesMcpInspection,
39
39
  inspect_hermes_mcp,
40
40
  is_absolute_memleaf_command,
41
+ memleaf_commands_same_runtime,
41
42
  )
42
43
  from .locking import atomic_write_json
43
44
  from .native_registration import ensure_hermes_native_sources
@@ -226,6 +227,33 @@ def _memleaf_mcp_command() -> Path:
226
227
  raise RuntimeError("memleaf-mcp console entry point was not found after package installation")
227
228
 
228
229
 
230
+ def _provider_mcp_command(runtime_command: Path | str) -> Path:
231
+ """Return the console entry point used by the copied MemoryProvider."""
232
+
233
+ runtime = Path(runtime_command).expanduser().resolve()
234
+ if os.name != "nt":
235
+ return runtime
236
+ if runtime.name.casefold() == "memleaf-mcp.exe":
237
+ return runtime
238
+ if runtime.name.casefold() == "memleaf-mcpw.exe":
239
+ console = runtime.with_name("memleaf-mcp.exe")
240
+ if console.is_file():
241
+ return console.resolve()
242
+ raise RuntimeError("the selected memleaf runtime has no memleaf-mcp.exe console entry point")
243
+
244
+
245
+ def _hermes_public_mcp_command(runtime_command: Path | str) -> Path:
246
+ """Return the no-console public MCP launcher for the selected runtime."""
247
+
248
+ provider_command = _provider_mcp_command(runtime_command)
249
+ if os.name != "nt":
250
+ return provider_command
251
+ gui = provider_command.with_name("memleaf-mcpw.exe")
252
+ if not gui.is_file():
253
+ raise RuntimeError("the selected memleaf runtime has no memleaf-mcpw.exe GUI entry point")
254
+ return gui.resolve()
255
+
256
+
229
257
  def _copy_provider(hermes_home: Path) -> Path:
230
258
  plugins = hermes_home / "plugins"
231
259
  target = plugins / "memleaf"
@@ -481,6 +509,15 @@ def _configure_hermes_mcp_entry(
481
509
  command=[detection.executable, "mcp", "list"],
482
510
  )
483
511
  allowed = inspection.status in {"absent", "legacy"}
512
+ same_runtime_launcher_migration = bool(
513
+ inspection.status == "runtime_conflict"
514
+ and inspection.configured_command
515
+ and memleaf_commands_same_runtime(
516
+ inspection.configured_command, command, platform=platform
517
+ )
518
+ )
519
+ if same_runtime_launcher_migration:
520
+ allowed = True
484
521
  if inspection.status == "runtime_conflict" and allow_runtime_migration:
485
522
  allowed = True
486
523
  if not allowed:
@@ -779,6 +816,7 @@ def install_hermes(
779
816
  selected_vault,
780
817
  current_command,
781
818
  platform=platform,
819
+ allow_same_runtime=True,
782
820
  )
783
821
  selected_command, runtime_details = _choose_hermes_mcp_command(
784
822
  preflight,
@@ -806,6 +844,26 @@ def install_hermes(
806
844
  user_action=action,
807
845
  )
808
846
 
847
+ try:
848
+ provider_command = _provider_mcp_command(selected_command)
849
+ public_command = _hermes_public_mcp_command(selected_command)
850
+ except RuntimeError as error:
851
+ return _failure_result(
852
+ stage="runtime_launcher",
853
+ reason=str(error),
854
+ core_version=core_version,
855
+ vault=selected_vault,
856
+ vault_source=vault_source,
857
+ mcp_runtime=runtime_details,
858
+ )
859
+ runtime_details = dict(runtime_details)
860
+ runtime_details.update(
861
+ {
862
+ "provider_command": str(provider_command),
863
+ "public_command": str(public_command),
864
+ }
865
+ )
866
+
809
867
  vault = Vault.initialize(selected_vault)
810
868
  model = _prepare_model_route(
811
869
  vault.root,
@@ -872,7 +930,7 @@ def install_hermes(
872
930
  adapter,
873
931
  detection,
874
932
  vault.root,
875
- selected_command,
933
+ str(public_command),
876
934
  allow_runtime_migration=(
877
935
  mcp_runtime == "current" and preflight.status == "runtime_conflict"
878
936
  ),
@@ -888,7 +946,7 @@ def install_hermes(
888
946
  ),
889
947
  recovery_commands=_mcp_recovery_commands(
890
948
  detection.executable,
891
- selected_command,
949
+ str(public_command),
892
950
  vault.root,
893
951
  ),
894
952
  )
@@ -899,7 +957,7 @@ def install_hermes(
899
957
  mcp=configured.to_dict(),
900
958
  recovery_commands=_mcp_recovery_commands(
901
959
  detection.executable,
902
- selected_command,
960
+ str(public_command),
903
961
  vault.root,
904
962
  ),
905
963
  )
@@ -933,7 +991,7 @@ def install_hermes(
933
991
  ),
934
992
  )
935
993
  try:
936
- _write_provider_config(provider_config, Path(selected_command), vault.root)
994
+ _write_provider_config(provider_config, provider_command, vault.root)
937
995
  except Exception as error:
938
996
  raise _HermesInstallFailure(
939
997
  "provider_config",
@@ -1015,7 +1073,8 @@ def install_hermes(
1015
1073
  "vault": str(vault.root),
1016
1074
  "vault_source": vault_source,
1017
1075
  "provider": str(provider_path),
1018
- "mcp_command": selected_command,
1076
+ "mcp_command": str(public_command),
1077
+ "provider_mcp_command": str(provider_command),
1019
1078
  "mcp_runtime": runtime_details,
1020
1079
  "model": model,
1021
1080
  "native_sources": native_registration,
@@ -6,6 +6,7 @@ import json
6
6
  import inspect
7
7
  import math
8
8
  import socket
9
+ import threading
9
10
  import urllib.error
10
11
  import urllib.request
11
12
  from typing import Any, Callable, Mapping, Optional, Protocol
@@ -246,10 +247,19 @@ class HTTPModelBackend:
246
247
  self.model = model
247
248
  self.timeout = normalize_request_timeout(timeout)
248
249
  self._opener = opener or urllib.request.urlopen
250
+ self._call_metrics_local = threading.local()
249
251
  # The built-in stateless urllib transport can be used concurrently.
250
252
  # An injected opener is caller-owned and therefore defaults to serial.
251
253
  self.parallel_safe = opener is None
252
254
 
255
+ def _set_call_metrics(self, value: Mapping[str, Any] | None) -> None:
256
+ self._call_metrics_local.value = dict(value) if isinstance(value, Mapping) else {}
257
+
258
+ def consume_call_metrics(self) -> dict[str, Any]:
259
+ value = getattr(self._call_metrics_local, "value", {})
260
+ self._call_metrics_local.value = {}
261
+ return dict(value) if isinstance(value, Mapping) else {}
262
+
253
263
  @staticmethod
254
264
  def _is_timeout_reason(value: Any) -> bool:
255
265
  if isinstance(value, (TimeoutError, socket.timeout)):
@@ -20,10 +20,12 @@ class OpenAICompatibleBackend(HTTPModelBackend):
20
20
  opener: Optional[Callable[..., Any]] = None,
21
21
  json_mode: bool = False,
22
22
  provider_name: str = "openai",
23
+ thinking: Mapping[str, Any] | None = None,
23
24
  ):
24
25
  super().__init__(base_url=base_url, api_key=api_key, model=model, timeout=timeout, opener=opener)
25
26
  self.json_mode = bool(json_mode)
26
27
  self.provider_name = provider_name.casefold() if isinstance(provider_name, str) else "openai"
28
+ self.thinking = dict(thinking) if isinstance(thinking, Mapping) else {}
27
29
 
28
30
  @staticmethod
29
31
  def _response_text_chars(value: Any) -> int:
@@ -83,7 +85,33 @@ class OpenAICompatibleBackend(HTTPModelBackend):
83
85
  "reasoning_chars": reasoning_chars,
84
86
  }
85
87
 
88
+ def _thinking_mode(self, purpose: str) -> str:
89
+ if purpose not in {"gate", "summarize", "compact"}:
90
+ return "default"
91
+ value = self.thinking.get(purpose, "low")
92
+ return value if value in {"default", "disabled", "low", "high", "max"} else "low"
93
+
94
+ @staticmethod
95
+ def _usage_metrics(value: Mapping[str, Any], *, thinking_mode: str) -> dict[str, Any]:
96
+ usage = value.get("usage")
97
+ result: dict[str, Any] = {"thinking_mode": thinking_mode}
98
+ if not isinstance(usage, Mapping):
99
+ return result
100
+ for key in (
101
+ "prompt_tokens", "completion_tokens", "total_tokens",
102
+ "prompt_cache_hit_tokens", "prompt_cache_miss_tokens",
103
+ ):
104
+ item = usage.get(key)
105
+ if isinstance(item, int) and not isinstance(item, bool) and 0 <= item <= 10_000_000:
106
+ result[key] = item
107
+ details = usage.get("completion_tokens_details")
108
+ reasoning = details.get("reasoning_tokens") if isinstance(details, Mapping) else None
109
+ if isinstance(reasoning, int) and not isinstance(reasoning, bool) and 0 <= reasoning <= 10_000_000:
110
+ result["reasoning_tokens"] = reasoning
111
+ return result
112
+
86
113
  def complete(self, prompt: str, *, system: str = "", purpose: str = "", temperature: float = 0.0) -> str:
114
+ self._set_call_metrics({})
87
115
  messages = []
88
116
  if system:
89
117
  messages.append({"role": "system", "content": system})
@@ -93,6 +121,13 @@ class OpenAICompatibleBackend(HTTPModelBackend):
93
121
  "messages": messages,
94
122
  "temperature": temperature,
95
123
  }
124
+ thinking_mode = self._thinking_mode(purpose)
125
+ if self.provider_name == "deepseek" and thinking_mode != "default":
126
+ if thinking_mode == "disabled":
127
+ payload["thinking"] = {"type": "disabled"}
128
+ else:
129
+ payload["thinking"] = {"type": "enabled"}
130
+ payload["reasoning_effort"] = thinking_mode
96
131
  if self.json_mode and purpose in {"gate", "summarize", "compact"}:
97
132
  payload["response_format"] = {"type": "json_object"}
98
133
  value = self._post_json(
@@ -117,6 +152,7 @@ class OpenAICompatibleBackend(HTTPModelBackend):
117
152
  stage=purpose,
118
153
  validation_reason="response_shape",
119
154
  )
155
+ self._set_call_metrics(self._usage_metrics(value, thinking_mode=thinking_mode))
120
156
  try:
121
157
  return self._text(message.get("content"), stage=purpose)
122
158
  except ModelError as error:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import os
7
+ import threading
7
8
  from typing import Any, Callable, Mapping, Optional
8
9
 
9
10
  from .base import (
@@ -44,6 +45,7 @@ class ModelRouter:
44
45
  self.host = self._coerce_host(host)
45
46
  self.api = self._coerce_api(api) if api is not None else self._build_api()
46
47
  self.diagnostics: list[dict[str, str]] = []
48
+ self._call_metrics_local = threading.local()
47
49
 
48
50
  @classmethod
49
51
  def from_config(cls, config: Mapping[str, Any], **kwargs: Any) -> "ModelRouter":
@@ -122,6 +124,7 @@ class ModelRouter:
122
124
  **kwargs,
123
125
  json_mode=provider in _JSON_MODE_PROVIDERS,
124
126
  provider_name=provider or "openai",
127
+ thinking=config.get("thinking") if isinstance(config.get("thinking"), Mapping) else None,
125
128
  )
126
129
  except (ModelError, ValueError, TypeError):
127
130
  return None
@@ -142,6 +145,7 @@ class ModelRouter:
142
145
  return str(getattr(backend, "provider", "unknown")), str(getattr(backend, "model", "unknown"))
143
146
 
144
147
  def _call(self, backend: ModelBackend, prompt: str, *, system: str, purpose: str, temperature: float) -> str:
148
+ self._call_metrics_local.value = {}
145
149
  try:
146
150
  value = backend.complete(prompt, system=system, purpose=purpose, temperature=temperature)
147
151
  except ModelError as error:
@@ -149,10 +153,23 @@ class ModelRouter:
149
153
  raise
150
154
  except Exception as error:
151
155
  raise ModelError("model backend failed", stage=purpose) from error
156
+ finally:
157
+ consume = getattr(backend, "consume_call_metrics", None)
158
+ if callable(consume):
159
+ try:
160
+ metrics = consume()
161
+ except Exception:
162
+ metrics = {}
163
+ self._call_metrics_local.value = dict(metrics) if isinstance(metrics, Mapping) else {}
152
164
  if not isinstance(value, str):
153
165
  raise ModelError("model backend returned non-text output", code="model_invalid_response", stage=purpose)
154
166
  return value
155
167
 
168
+ def consume_call_metrics(self) -> dict[str, Any]:
169
+ value = getattr(self._call_metrics_local, "value", {})
170
+ self._call_metrics_local.value = {}
171
+ return dict(value) if isinstance(value, Mapping) else {}
172
+
156
173
  def complete(self, prompt: str, *, system: str = "", purpose: str = "", temperature: float = 0.0) -> str:
157
174
  if not isinstance(prompt, str):
158
175
  raise TypeError("model prompt must be text")