flurryx-code-memory 0.6.2__tar.gz → 0.7.0__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 (201) hide show
  1. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/CHANGELOG.md +201 -0
  2. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/PKG-INFO +1 -1
  3. {flurryx_code_memory-0.6.2/plugins/cursor → flurryx_code_memory-0.7.0/plugins/claude-code}/scripts/lib/memory.js +85 -0
  4. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-session-start.js +12 -2
  5. {flurryx_code_memory-0.6.2/plugins/claude-code → flurryx_code_memory-0.7.0/plugins/cursor}/scripts/lib/memory.js +85 -0
  6. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-session-start.js +10 -0
  7. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/pyproject.toml +1 -1
  8. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/cli.py +50 -8
  9. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/config.py +170 -5
  10. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/embed/ollama.py +18 -1
  11. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/graph/falkor_store.py +72 -1
  12. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/mcp_server.py +140 -8
  13. flurryx_code_memory-0.7.0/src/code_memory/orchestrator/ingest_state.py +117 -0
  14. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/pipeline.py +305 -26
  15. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/autostart/base.py +5 -4
  16. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/autostart/launchd.py +2 -2
  17. flurryx_code_memory-0.7.0/src/code_memory/sync/safety.py +182 -0
  18. flurryx_code_memory-0.7.0/src/code_memory/sync/single_flight.py +291 -0
  19. flurryx_code_memory-0.7.0/tests/test_config_ipv4_defaults.py +117 -0
  20. flurryx_code_memory-0.7.0/tests/test_embed_ollama_timeout.py +152 -0
  21. flurryx_code_memory-0.7.0/tests/test_ensure_fresh_no_blocking.py +408 -0
  22. flurryx_code_memory-0.7.0/tests/test_graph_shadow_swap.py +304 -0
  23. flurryx_code_memory-0.7.0/tests/test_ingest_safety_and_lock.py +215 -0
  24. flurryx_code_memory-0.7.0/tests/test_ingest_state.py +131 -0
  25. flurryx_code_memory-0.7.0/tests/test_pipeline_health_check.py +192 -0
  26. flurryx_code_memory-0.7.0/tests/test_worktree_autostart_guard.py +293 -0
  27. flurryx_code_memory-0.7.0/tests/test_worktree_slug.py +225 -0
  28. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/uv.lock +1 -1
  29. flurryx_code_memory-0.6.2/src/code_memory/orchestrator/ingest_state.py +0 -71
  30. flurryx_code_memory-0.6.2/src/code_memory/sync/safety.py +0 -93
  31. flurryx_code_memory-0.6.2/tests/test_ingest_state.py +0 -62
  32. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/.claude-plugin/marketplace.json +0 -0
  33. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/.env.example +0 -0
  34. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/.gitignore +0 -0
  35. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/README.md +0 -0
  36. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docker/docker-compose.yml +0 -0
  37. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/BENCHMARK.md +0 -0
  38. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/BENCHMARK_VS_BASELINE.json +0 -0
  39. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/BENCHMARK_VS_BASELINE.md +0 -0
  40. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/architecture.png +0 -0
  41. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/benchmark-raw.json +0 -0
  42. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/docs/hero.png +0 -0
  43. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/install.ps1 +0 -0
  44. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/install.sh +0 -0
  45. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  46. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/README.md +0 -0
  47. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/commands/code-memory.md +0 -0
  48. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/hooks/hooks.json +0 -0
  49. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/install.sh +0 -0
  50. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/lib/claim-intent.js +0 -0
  51. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/lib/claim-intent.test.js +0 -0
  52. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/lib/io.js +0 -0
  53. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/lib/state.js +0 -0
  54. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-post-tool.js +0 -0
  55. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-pre-tool.js +0 -0
  56. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-retrieve-seen.js +0 -0
  57. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-stop.js +0 -0
  58. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/on-user-prompt.js +0 -0
  59. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/scripts/resolver-debounce.js +0 -0
  60. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/claude-code/skills/code-memory/SKILL.md +0 -0
  61. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/README.md +0 -0
  62. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/hooks/hooks.json.template +0 -0
  63. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/install.sh +0 -0
  64. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/rules/code-memory.mdc +0 -0
  65. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/lib/claim-intent.js +0 -0
  66. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/lib/claim-intent.test.js +0 -0
  67. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/lib/io.js +0 -0
  68. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/lib/state.js +0 -0
  69. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-after-file-edit.js +0 -0
  70. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-before-mcp-execution.js +0 -0
  71. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-before-submit-prompt.js +0 -0
  72. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-post-tool-use.js +0 -0
  73. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-pre-compact.js +0 -0
  74. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-pre-tool-use.js +0 -0
  75. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-session-end.js +0 -0
  76. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/on-stop.js +0 -0
  77. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/cursor/scripts/resolver-debounce.js +0 -0
  78. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/README.md +0 -0
  79. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/install.sh +0 -0
  80. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/package-lock.json +0 -0
  81. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/package.json +0 -0
  82. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/scripts/add-mcp.py +0 -0
  83. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/scripts/install.mjs +0 -0
  84. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/scripts/uninstall.mjs +0 -0
  85. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/skills/code-memory/SKILL.md +0 -0
  86. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/src/code-memory-lib/claim-intent.test.mts +0 -0
  87. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/src/code-memory-lib/claim-intent.ts +0 -0
  88. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/src/code-memory-lib/memory-client.ts +0 -0
  89. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/src/code-memory.ts +0 -0
  90. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/opencode/tsconfig.json +0 -0
  91. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/vibe/README.md +0 -0
  92. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/vibe/install.sh +0 -0
  93. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/plugins/vibe/skills/code-memory/SKILL.md +0 -0
  94. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/benchmark.py +0 -0
  95. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/benchmark_queries.json +0 -0
  96. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/benchmark_vs_baseline.py +0 -0
  97. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/benchmark_vs_grep.sh +0 -0
  98. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/ingest.py +0 -0
  99. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/install.ps1 +0 -0
  100. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/scripts/install.sh +0 -0
  101. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/__init__.py +0 -0
  102. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/claims/__init__.py +0 -0
  103. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/claims/extractor.py +0 -0
  104. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/claims/indexer.py +0 -0
  105. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/claims/resolver.py +0 -0
  106. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/claims/store.py +0 -0
  107. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/embed/__init__.py +0 -0
  108. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/embed/cache.py +0 -0
  109. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/embed/m3.py +0 -0
  110. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/embed/tei.py +0 -0
  111. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/episodic/__init__.py +0 -0
  112. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/episodic/sqlite_store.py +0 -0
  113. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/__init__.py +0 -0
  114. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/csproj.py +0 -0
  115. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/dll.py +0 -0
  116. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/gitignore.py +0 -0
  117. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/nuget.py +0 -0
  118. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/sanity.py +0 -0
  119. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/sln.py +0 -0
  120. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/extractor/treesitter.py +0 -0
  121. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/graph/__init__.py +0 -0
  122. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/metrics.py +0 -0
  123. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/__init__.py +0 -0
  124. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/git_delta.py +0 -0
  125. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/reset.py +0 -0
  126. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/resolver.py +0 -0
  127. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/orchestrator/retrieve.py +0 -0
  128. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/resilience.py +0 -0
  129. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/__init__.py +0 -0
  130. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/autostart/__init__.py +0 -0
  131. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/autostart/schtasks.py +0 -0
  132. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/autostart/systemd.py +0 -0
  133. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/hooks.py +0 -0
  134. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/snapshot.py +0 -0
  135. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/store.py +0 -0
  136. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/sync.py +0 -0
  137. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/sync/watcher.py +0 -0
  138. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/updater.py +0 -0
  139. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/vector/__init__.py +0 -0
  140. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/src/code_memory/vector/qdrant_store.py +0 -0
  141. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_autostart_adapters.py +0 -0
  142. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_chunk_text.py +0 -0
  143. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_claim_extractor.py +0 -0
  144. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_claim_indexer.py +0 -0
  145. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_claim_resolver.py +0 -0
  146. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_claim_store.py +0 -0
  147. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_config_embed_dim.py +0 -0
  148. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_config_sentinel.py +0 -0
  149. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_csproj.py +0 -0
  150. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_dll_members.py +0 -0
  151. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_dll_parser.py +0 -0
  152. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_embed_backend.py +0 -0
  153. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_embed_cache.py +0 -0
  154. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_embed_m3.py +0 -0
  155. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_embed_tei.py +0 -0
  156. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_episode_dedup.py +0 -0
  157. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_episode_head_sha.py +0 -0
  158. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_csharp.py +0 -0
  159. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_dart.py +0 -0
  160. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_filters.py +0 -0
  161. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_php.py +0 -0
  162. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_python_imports.py +0 -0
  163. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_receiver_type.py +0 -0
  164. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_references.py +0 -0
  165. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_sanity.py +0 -0
  166. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_ts_abstract.py +0 -0
  167. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_ts_inject.py +0 -0
  168. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_extractor_utf8.py +0 -0
  169. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_file_containment.py +0 -0
  170. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_git_delta.py +0 -0
  171. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_graph_queries.py +0 -0
  172. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_graph_temporal.py +0 -0
  173. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_graph_vacuum_at_sha.py +0 -0
  174. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_hooks_installer.py +0 -0
  175. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_mcp_assert_claim.py +0 -0
  176. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_mcp_server_descriptions.py +0 -0
  177. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_mcp_shutdown.py +0 -0
  178. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_mcp_strict_project.py +0 -0
  179. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_metrics.py +0 -0
  180. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_nuget_resolver.py +0 -0
  181. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_overload_resolution.py +0 -0
  182. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_partial_class.py +0 -0
  183. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_pipeline_references.py +0 -0
  184. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_pipeline_temporal_wiring.py +0 -0
  185. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_qdrant_legacy_guard.py +0 -0
  186. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_razor_inject.py +0 -0
  187. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_resilience.py +0 -0
  188. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_resolver.py +0 -0
  189. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_resolver_assembly.py +0 -0
  190. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_retrieve_claims_surfacing.py +0 -0
  191. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_retrieve_rerank.py +0 -0
  192. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_sln.py +0 -0
  193. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_smoke.py +0 -0
  194. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_snapshot_e2e.py +0 -0
  195. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_snapshot_format.py +0 -0
  196. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_snapshot_store.py +0 -0
  197. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_sync_decision_tree.py +0 -0
  198. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_watch_safety.py +0 -0
  199. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_watcher_debouncer.py +0 -0
  200. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_watcher_exclude.py +0 -0
  201. {flurryx_code_memory-0.6.2 → flurryx_code_memory-0.7.0}/tests/test_watcher_ref_events.py +0 -0
@@ -8,6 +8,207 @@ when the repo grows.
8
8
  This file complements `git log`: commits explain mechanics, this file
9
9
  explains intent.
10
10
 
11
+ ## [0.7.0] — 2026-06-12
12
+
13
+ Release theme: **Worktree resilience, atomic graph rebuilds, and fresh
14
+ indexes**. The index now survives interrupted full ingests; linked git
15
+ worktrees reuse the main repo's index instead of forcing a cold re-ingest;
16
+ and queries trigger non-blocking background rebuilds when the index drifts
17
+ from HEAD.
18
+
19
+ ### Added — Linked git worktree awareness
20
+
21
+ **What:** a new `_git_toplevel()` helper resolves linked git worktrees
22
+ to the main repo's root by running `git rev-parse --git-common-dir`
23
+ after the baseline `--show-toplevel` call. In a linked worktree, the
24
+ two paths differ; in the main worktree, they're the same. The project
25
+ slug is now derived from the main repo's directory name, so all worktrees
26
+ of the same project reuse the same Qdrant / Falkor namespace without
27
+ requiring a cold re-ingest.
28
+
29
+ **Reason:** developers working in linked worktrees (e.g. `git worktree add
30
+ ../feature-branch`) saw no code-memory functionality because the ingest
31
+ system minted a separate Qdrant collection and Falkor graph for each
32
+ worktree. A single repo with 3–5 active worktrees accumulated 3–5 cold
33
+ ingests. The main repo and each worktree now share the same index.
34
+
35
+ ### Added — Non-persistent autostart for linked worktrees
36
+
37
+ **What:** two new gates in the autostart system:
38
+
39
+ - `is_linked_git_worktree(path)` — checks whether *path* is inside a
40
+ linked worktree via git CLI.
41
+ - `is_non_persistent_watch_dir(path)` — returns True for ephemeral
42
+ session dirs OR linked worktrees; used by `ensure_autostart()` to skip
43
+ registering a persistent OS agent for directories that are temporary or
44
+ share the main repo's watcher.
45
+ - `LaunchdAdapter.prune_stale()` (run on every MCP bootstrap) removes
46
+ launchd agents whose `WorkingDirectory` is gone or is a linked worktree.
47
+
48
+ **Reason:** the prior release fixed the watcher's per-session accumulation;
49
+ this release prevents the same bleed on linked worktrees. A developer with
50
+ 2 linked worktrees no longer gets 3 persistent `code-memory watch` units.
51
+
52
+ ### Added — Shadow-graph atomic promotion (graph durability fix)
53
+
54
+ **What:** the full ingest pipeline now builds into a shadow FalkorDB
55
+ graph named `<project_graph>__shadow` and atomically promotes it only
56
+ after the rebuild succeeds:
57
+
58
+ 1. At ingest start, drop any leftover shadow from a prior interrupted
59
+ rebuild.
60
+ 2. Redirect all graph writes to the shadow FalkorStore instance.
61
+ 3. On successful completion, execute `GRAPH.DELETE <live>`, then
62
+ `GRAPH.COPY <shadow> <live>`, then `GRAPH.DELETE <shadow>`.
63
+ 4. If the copy fails, the live graph is cleared but the shadow stays
64
+ intact — the caller can retry without losing data.
65
+
66
+ **Reason:** before this, an interrupted full ingest (network loss,
67
+ Ollama timeout, Falkor down, user kills the process) left the live
68
+ graph empty so subsequent `callers` / `definitions` / `callees` queries
69
+ returned nothing until a manual full re-ingest. The graph now survives
70
+ interruption — the shadow is cleaned up on the next rebuild attempt.
71
+
72
+ ### Added — Health-check guard for stale rebuilds
73
+
74
+ **What:** the ingest state now records `file_count` and `symbol_count`
75
+ from each successful full rebuild. Before starting an incremental ingest,
76
+ `_health_check_ok()` compares the current ingestable file count against
77
+ the stored baseline; if it grew more than 20% and the graph symbol count
78
+ is suspiciously low (below a ratio threshold), a full rebuild is forced
79
+ with a diagnostic message to stderr.
80
+
81
+ Config:
82
+ - `CODE_MEMORY_INGEST_HEALTH_CHECK_ENABLED` (default `true`)
83
+ - `CODE_MEMORY_INGEST_HEALTH_CHECK_MIN_RATIO` (default `0.3` = expect ≥30%
84
+ of file count as symbol count)
85
+
86
+ **Reason:** a transient FalkorDB outage or an incomplete prior ingest
87
+ could leave the graph permanently empty while incremental ingests reported
88
+ success. The health check detects this silently-failed state and forces a
89
+ rebuild, with no user intervention needed.
90
+
91
+ ### Added — Single-flight ingest lock
92
+
93
+ **What:** new `src/code_memory/sync/single_flight.py` module provides
94
+ in-process (`asyncio.Lock`) + cross-process (PID file) guards to prevent
95
+ concurrent full ingests for the same (root, project) pair.
96
+
97
+ - `try_acquire(root, project)` — returns True if no rebuild is running,
98
+ False if the slot is taken.
99
+ - `release(root, project)` — releases the slot unconditionally.
100
+ - Stale PID files (dead process or age > 30 min) are silently removed.
101
+
102
+ The Claude Code and Cursor plugins' `on-session-start.js` fast-path-skip a
103
+ spawn when a live ingest is detected, preventing thundering-herd `code-memory
104
+ ingest` calls on boot.
105
+
106
+ **Reason:** on slow machines or large repos, overlapping `code-memory ingest`
107
+ calls could queue up and queue up (especially if the embedder is I/O-bound),
108
+ causing a session to take 5+ minutes to boot. The lock ensures at most one
109
+ rebuild runs; fast-path skips spare the overhead.
110
+
111
+ ### Added — Ingest safety guards
112
+
113
+ **What:** new `assert_safe_ingest_root()` function in `sync/safety.py`
114
+ refuses to ingest:
115
+
116
+ 1. System/HOME roots (same set as the watcher guard: HOME, /, /tmp,
117
+ /var, /etc, /usr, /System, /Library, /opt, /Applications, C:/, etc.)
118
+ 2. Non-git directories (checked via `is_inside_git_worktree()`).
119
+
120
+ Bypass via `CODE_MEMORY_UNSAFE_INGEST=1` env var (env-only, not a CLI
121
+ flag, to prevent accidental use).
122
+
123
+ **Reason:** `code-memory ingest ~` could walk every IDE cache, checkout,
124
+ and node_modules on disk. `ingest` is more dangerous than `watch` because
125
+ it stores results; a non-git directory ingest mints an arbitrary project
126
+ slug. The guard is invoked by the CLI `ingest` entry point and by hooks.
127
+
128
+ ### Added — IPv4-default service URLs
129
+
130
+ **What:** the default `OLLAMA_URL`, `QDRANT_URL`, and `FALKOR_URL` now
131
+ use `127.0.0.1` instead of `localhost`. This works around a Windows
132
+ quirk where `localhost` may resolve to `::1` (IPv6) and hang on socket
133
+ connect.
134
+
135
+ Config example (all in `.code-memoryrc` or env):
136
+ ```
137
+ OLLAMA_URL=http://127.0.0.1:11434
138
+ QDRANT_URL=http://127.0.0.1:6333
139
+ FALKOR_URL=redis://127.0.0.1:6379
140
+ ```
141
+
142
+ **Reason:** on some Windows setups, the TCP stack prefers IPv6 and
143
+ binds localhost to `::1`, while the service listens on `127.0.0.1`.
144
+ Callers then hang waiting for a timeout (seconds to minutes). Using
145
+ the explicit IPv4 address is more reliable.
146
+
147
+ ### Added — Non-blocking `_ensure_fresh` for MCP queries
148
+
149
+ **What:** the pre-query guard `_ensure_fresh()` no longer blocks on a
150
+ sync. Instead:
151
+
152
+ 1. Spawn a quick freshness check (`_is_index_stale()`) in a bounded
153
+ daemon thread (`_FRESHNESS_PROBE_TIMEOUT`, default 2.0 s).
154
+ 2. If the index is stale AND the check finished in time, fire a
155
+ background `_background_rebuild()` in a detached daemon thread
156
+ protected by the single-flight lock.
157
+ 3. Return immediately so the query gets the current (possibly stale)
158
+ index while the rebuild runs in the background.
159
+
160
+ **Reason:** MCP queries were blocking on a full ingest in the worst case,
161
+ causing Claude Code / OpenCode / Cursor to hang for minutes. Now the agent
162
+ gets an answer immediately while background sync keeps the index fresh.
163
+
164
+ ### Fixed — Ollama embed connect timeout
165
+
166
+ **What:** `OllamaEmbedder` now uses split connect/read timeouts:
167
+
168
+ - `_DEFAULT_CONNECT_TIMEOUT = 5.0 s` — fail fast on wrong stack (IPv6
169
+ vs IPv4) or misconfigured host.
170
+ - `_DEFAULT_READ_TIMEOUT = 300.0 s` — Ollama's cold-load model phase
171
+ happens during read, not connect.
172
+
173
+ Configurable via `OLLAMA_CONNECT_TIMEOUT` and `OLLAMA_READ_TIMEOUT`
174
+ env vars.
175
+
176
+ **Reason:** the old single `timeout=300` param applied to connect, which
177
+ could hang for 300 s waiting on a misconfigured IPv6 address. A 5 s
178
+ connect timeout with 3 retries fails fast (~15 s worst case) instead.
179
+
180
+ ### Added — Vibe plugin
181
+
182
+ **What:** new `plugins/vibe/` brings code-memory to Mistral Vibe. Vibe
183
+ lacks lifecycle hooks (unlike Claude Code, Cursor, OpenCode), so the
184
+ plugin delivers:
185
+
186
+ - **Skill** (`/code-memory`) with orientation guidance and manual command
187
+ runner.
188
+ - **MCP server** registration in `config.toml`.
189
+ - **OS autostart watcher** for file-edit → auto-reingest (since hooks
190
+ aren't available).
191
+
192
+ Install: `./plugins/vibe/install.sh` (default user scope, with flags for
193
+ project scope, no-mcp, no-watch, uninstall).
194
+
195
+ **Reason:** Vibe is a code-aware LLM editor with a different extension
196
+ model. Bundling the same code-memory integration surfaces our topology
197
+ queries to Vibe users without reimplementation.
198
+
199
+ ### Added — Ingest state enhancements
200
+
201
+ **What:** the ingest state now stores:
202
+
203
+ - `file_count` / `symbol_count` from each full rebuild (used by the
204
+ health check).
205
+ - `file_count` and `symbol_count` are populated by the pipeline during
206
+ ingest and read by the health-check predicate before deciding to
207
+ rebuild.
208
+
209
+ **Reason:** enables the health-check guard to detect silently-failed
210
+ ingests.
211
+
11
212
  ## [0.6.0] — 2026-06-04
12
213
 
13
214
  Release theme: **Dart joins the graph, and `this.field.method()` resolves
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flurryx-code-memory
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Local lightweight memory layer for coding agents: FalkorDB + Qdrant + Ollama (BGE-M3) + tree-sitter
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: anyio>=4.4
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  const { execFile } = require("node:child_process");
10
+ const fs = require("node:fs");
11
+ const nodePath = require("node:path");
10
12
 
11
13
  const DEFAULT_BINARY = process.env.CODE_MEMORY_BIN || "code-memory";
12
14
  const DEFAULT_PROJECT = process.env.CODE_MEMORY_PROJECT || null;
@@ -39,6 +41,82 @@ async function detectAvailable(binary, log) {
39
41
  return ok;
40
42
  }
41
43
 
44
+ /**
45
+ * Derive the lock directory used by code-memory's single_flight module.
46
+ * Mirrors the Python logic in sync/single_flight.py:_lock_dir().
47
+ * Returns null if the directory cannot be determined.
48
+ */
49
+ function _lockDir() {
50
+ const override = process.env.CODE_MEMORY_LOCK_DIR;
51
+ if (override) return override;
52
+ const stateHome =
53
+ process.env.XDG_STATE_HOME ||
54
+ nodePath.join(
55
+ process.env.HOME || process.env.USERPROFILE || "",
56
+ ".local",
57
+ "state",
58
+ );
59
+ return nodePath.join(stateHome, "code-memory", "locks");
60
+ }
61
+
62
+ /**
63
+ * Fast-path check: return true if a live ingest is already running for
64
+ * (resolvedRoot, slug). Mirrors Python single_flight._is_stale() logic:
65
+ * a lockfile is considered live when it exists, is not older than the TTL,
66
+ * and its PID is still running.
67
+ *
68
+ * This is JS-side best-effort only — the Python ingest entry point is the
69
+ * authoritative single-flight guard. We check here solely to avoid
70
+ * spawning a new process that would immediately lose the race.
71
+ *
72
+ * @param {string} resolvedRoot - Absolute resolved path of the repo root.
73
+ * @param {string} slug - Project slug.
74
+ * @returns {boolean} true when a live ingest appears to be running.
75
+ */
76
+ function _ingestLockLive(resolvedRoot, slug) {
77
+ try {
78
+ const lockDir = _lockDir();
79
+ if (!lockDir) return false;
80
+
81
+ // Replicate the Python filename derivation:
82
+ // name = f"{root_part[:64]}__{project_part[:32]}.lock"
83
+ const rootPart = resolvedRoot.replace(/[/\\]/g, "_").replace(/ /g, "_").slice(0, 64);
84
+ const slugPart = slug.replace(/\//g, "_").replace(/ /g, "_").slice(0, 32);
85
+ const lockFile = nodePath.join(lockDir, `${rootPart}__${slugPart}.lock`);
86
+
87
+ let stat;
88
+ try {
89
+ stat = fs.statSync(lockFile);
90
+ } catch {
91
+ return false; // file does not exist — no live ingest
92
+ }
93
+
94
+ const ttl = parseFloat(process.env.CODE_MEMORY_REBUILD_LOCK_TTL || "3600");
95
+ const ageSecs = (Date.now() - stat.mtimeMs) / 1000;
96
+ if (ageSecs > ttl) return false; // stale by age
97
+
98
+ let pid;
99
+ try {
100
+ pid = parseInt(fs.readFileSync(lockFile, "utf8").trim(), 10);
101
+ } catch {
102
+ return false; // unreadable → treat as stale
103
+ }
104
+ if (!pid || isNaN(pid)) return false;
105
+
106
+ // Check if the PID is alive (POSIX: signal 0; Windows: tasklist not used,
107
+ // fall back to optimistic "assume live" to avoid a subprocess spawn).
108
+ try {
109
+ process.kill(pid, 0);
110
+ return true; // PID exists and is alive
111
+ } catch (e) {
112
+ if (e.code === "EPERM") return true; // exists but we lack permission
113
+ return false; // ESRCH — process is dead
114
+ }
115
+ } catch {
116
+ return false; // any unexpected error → don't block
117
+ }
118
+ }
119
+
42
120
  /**
43
121
  * Spawn detached fire-and-forget. Parent exits immediately.
44
122
  * stdout/stderr ignored. Used when the hook must not block.
@@ -115,6 +193,13 @@ async function createMemoryClient(opts = {}) {
115
193
 
116
194
  ingestDetached({ full = false } = {}) {
117
195
  if (!available) return false;
196
+ // Fast-path: skip spawn if the Python single-flight lock shows a live
197
+ // ingest is already running for this root. The CLI is the authoritative
198
+ // guard; this avoids spawning a process that would immediately lose the
199
+ // race and exit with code 0.
200
+ const resolvedCwd = nodePath.resolve(cwd);
201
+ const slug = project || nodePath.basename(resolvedCwd);
202
+ if (_ingestLockLive(resolvedCwd, slug)) return false;
118
203
  return spawnDetached(
119
204
  binary,
120
205
  ["ingest", cwd, "--json", ...(full ? ["--full"] : []), ...baseArgs(project)],
@@ -25,8 +25,18 @@ const { pruneExpired } = require("./lib/state");
25
25
  const mem = await createMemoryClient({ cwd, log });
26
26
  if (mem.available) {
27
27
  // Ensure a launchd/systemd watcher unit exists for this repo so file
28
- // edits between sessions trigger reingest automatically. Idempotent;
29
- // safety guard inside the CLI refuses home/root / non-VCS dirs.
28
+ // edits between sessions trigger reingest automatically. Idempotent.
29
+ //
30
+ // Both calls delegate to the `code-memory` binary (DEFAULT_BINARY /
31
+ // CODE_MEMORY_BIN — see lib/memory.js). The CLI enforces its own
32
+ // safety guards at the Python entry point:
33
+ // • `autostart install` — rejects HOME / system roots / ephemeral dirs
34
+ // via sync/safety.py:assert_safe_watch_root (wired in cli.py:watch).
35
+ // • `ingest` — rejects HOME / filesystem roots / non-git dirs via
36
+ // sync/safety.py:assert_safe_ingest_root (wired in cli.py:ingest).
37
+ // A single-flight PID lock also prevents concurrent ingests for the
38
+ // same root (sync/single_flight.py).
39
+ // These guards are install-version-independent (PyPI, uv tool, editable).
30
40
  mem.autostartInstallDetached();
31
41
  mem.ingestDetached();
32
42
  }
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  const { execFile } = require("node:child_process");
10
+ const fs = require("node:fs");
11
+ const nodePath = require("node:path");
10
12
 
11
13
  const DEFAULT_BINARY = process.env.CODE_MEMORY_BIN || "code-memory";
12
14
  const DEFAULT_PROJECT = process.env.CODE_MEMORY_PROJECT || null;
@@ -39,6 +41,82 @@ async function detectAvailable(binary, log) {
39
41
  return ok;
40
42
  }
41
43
 
44
+ /**
45
+ * Derive the lock directory used by code-memory's single_flight module.
46
+ * Mirrors the Python logic in sync/single_flight.py:_lock_dir().
47
+ * Returns null if the directory cannot be determined.
48
+ */
49
+ function _lockDir() {
50
+ const override = process.env.CODE_MEMORY_LOCK_DIR;
51
+ if (override) return override;
52
+ const stateHome =
53
+ process.env.XDG_STATE_HOME ||
54
+ nodePath.join(
55
+ process.env.HOME || process.env.USERPROFILE || "",
56
+ ".local",
57
+ "state",
58
+ );
59
+ return nodePath.join(stateHome, "code-memory", "locks");
60
+ }
61
+
62
+ /**
63
+ * Fast-path check: return true if a live ingest is already running for
64
+ * (resolvedRoot, slug). Mirrors Python single_flight._is_stale() logic:
65
+ * a lockfile is considered live when it exists, is not older than the TTL,
66
+ * and its PID is still running.
67
+ *
68
+ * This is JS-side best-effort only — the Python ingest entry point is the
69
+ * authoritative single-flight guard. We check here solely to avoid
70
+ * spawning a new process that would immediately lose the race.
71
+ *
72
+ * @param {string} resolvedRoot - Absolute resolved path of the repo root.
73
+ * @param {string} slug - Project slug.
74
+ * @returns {boolean} true when a live ingest appears to be running.
75
+ */
76
+ function _ingestLockLive(resolvedRoot, slug) {
77
+ try {
78
+ const lockDir = _lockDir();
79
+ if (!lockDir) return false;
80
+
81
+ // Replicate the Python filename derivation:
82
+ // name = f"{root_part[:64]}__{project_part[:32]}.lock"
83
+ const rootPart = resolvedRoot.replace(/[/\\]/g, "_").replace(/ /g, "_").slice(0, 64);
84
+ const slugPart = slug.replace(/\//g, "_").replace(/ /g, "_").slice(0, 32);
85
+ const lockFile = nodePath.join(lockDir, `${rootPart}__${slugPart}.lock`);
86
+
87
+ let stat;
88
+ try {
89
+ stat = fs.statSync(lockFile);
90
+ } catch {
91
+ return false; // file does not exist — no live ingest
92
+ }
93
+
94
+ const ttl = parseFloat(process.env.CODE_MEMORY_REBUILD_LOCK_TTL || "3600");
95
+ const ageSecs = (Date.now() - stat.mtimeMs) / 1000;
96
+ if (ageSecs > ttl) return false; // stale by age
97
+
98
+ let pid;
99
+ try {
100
+ pid = parseInt(fs.readFileSync(lockFile, "utf8").trim(), 10);
101
+ } catch {
102
+ return false; // unreadable → treat as stale
103
+ }
104
+ if (!pid || isNaN(pid)) return false;
105
+
106
+ // Check if the PID is alive (POSIX: signal 0; Windows: tasklist not used,
107
+ // fall back to optimistic "assume live" to avoid a subprocess spawn).
108
+ try {
109
+ process.kill(pid, 0);
110
+ return true; // PID exists and is alive
111
+ } catch (e) {
112
+ if (e.code === "EPERM") return true; // exists but we lack permission
113
+ return false; // ESRCH — process is dead
114
+ }
115
+ } catch {
116
+ return false; // any unexpected error → don't block
117
+ }
118
+ }
119
+
42
120
  /**
43
121
  * Spawn detached fire-and-forget. Parent exits immediately.
44
122
  * stdout/stderr ignored. Used when the hook must not block.
@@ -115,6 +193,13 @@ async function createMemoryClient(opts = {}) {
115
193
 
116
194
  ingestDetached({ full = false } = {}) {
117
195
  if (!available) return false;
196
+ // Fast-path: skip spawn if the Python single-flight lock shows a live
197
+ // ingest is already running for this root. The CLI is the authoritative
198
+ // guard; this avoids spawning a process that would immediately lose the
199
+ // race and exit with code 0.
200
+ const resolvedCwd = nodePath.resolve(cwd);
201
+ const slug = project || nodePath.basename(resolvedCwd);
202
+ if (_ingestLockLive(resolvedCwd, slug)) return false;
118
203
  return spawnDetached(
119
204
  binary,
120
205
  ["ingest", cwd, "--json", ...(full ? ["--full"] : []), ...baseArgs(project)],
@@ -21,6 +21,16 @@ const { pruneExpired } = require("./lib/state");
21
21
 
22
22
  const mem = await createMemoryClient({ cwd, log: () => {} });
23
23
  if (mem.available) {
24
+ // Both calls delegate to the `code-memory` binary (DEFAULT_BINARY /
25
+ // CODE_MEMORY_BIN — see lib/memory.js). The CLI enforces its own
26
+ // safety guards at the Python entry point:
27
+ // • `autostart install` — rejects HOME / system roots / ephemeral dirs
28
+ // via sync/safety.py:assert_safe_watch_root (wired in cli.py:watch).
29
+ // • `ingest` — rejects HOME / filesystem roots / non-git dirs via
30
+ // sync/safety.py:assert_safe_ingest_root (wired in cli.py:ingest).
31
+ // A single-flight PID lock also prevents concurrent ingests for the
32
+ // same root (sync/single_flight.py).
33
+ // These guards are install-version-independent (PyPI, uv tool, editable).
24
34
  mem.ingestDetached();
25
35
  mem.autostartInstallDetached();
26
36
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flurryx-code-memory"
7
- version = "0.6.2"
7
+ version = "0.7.0"
8
8
  description = "Local lightweight memory layer for coding agents: FalkorDB + Qdrant + Ollama (BGE-M3) + tree-sitter"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -118,14 +118,37 @@ def ingest(
118
118
 
119
119
  Default: git-aware incremental — diff prior state to HEAD.
120
120
  """
121
- slug = project or detect_project_slug(root)
122
- pipe = Pipeline(project=slug, skip_vectors=no_vectors)
123
- stats = pipe.ingest_repo(
124
- root,
125
- mode="full" if full else "auto",
126
- since=since,
127
- dry_run=dry_run,
128
- )
121
+ from .sync.safety import UnsafeIngestRootError, assert_safe_ingest_root
122
+ from .sync.single_flight import release, try_acquire
123
+
124
+ # --- Phase 3a: refuse HOME / filesystem roots / non-git dirs ----------
125
+ try:
126
+ safe_root = assert_safe_ingest_root(root)
127
+ except UnsafeIngestRootError as exc:
128
+ typer.echo(f"error: {exc}", err=True)
129
+ raise typer.Exit(code=2) from exc
130
+
131
+ slug = project or detect_project_slug(safe_root)
132
+
133
+ # --- Phase 3b: single-flight lock — skip if an ingest is already live --
134
+ if not try_acquire(safe_root, slug):
135
+ typer.echo(
136
+ f"skipped: ingest already running for project={slug!r} root={safe_root}",
137
+ err=True,
138
+ )
139
+ raise typer.Exit(code=0)
140
+
141
+ try:
142
+ pipe = Pipeline(project=slug, skip_vectors=no_vectors)
143
+ stats = pipe.ingest_repo(
144
+ safe_root,
145
+ mode="full" if full else "auto",
146
+ since=since,
147
+ dry_run=dry_run,
148
+ )
149
+ finally:
150
+ release(safe_root, slug)
151
+
129
152
  _emit(
130
153
  {"project": slug, "dry_run": dry_run, "ingested": asdict(stats)},
131
154
  as_json=as_json,
@@ -289,6 +312,25 @@ def reingest(
289
312
  as_json: bool = JsonOpt,
290
313
  ) -> None:
291
314
  """Re-ingest a single file."""
315
+ from .config import is_inside_git_worktree
316
+
317
+ # --- Phase 4: skip files that are not inside any git worktree ---------
318
+ # This backstop catches edits to files under ~/.claude/..., C:\Users\...,
319
+ # or any other non-project path that the cwd-containment guard in the JS
320
+ # hook can't catch when cwd itself is not a git directory. Without this
321
+ # guard, detect_project_slug falls back to the raw directory name and
322
+ # mints parasitic Qdrant collections like "code_chunks__on-session-start-js".
323
+ if not is_inside_git_worktree(path.resolve().parent):
324
+ _emit(
325
+ {
326
+ "skipped": True,
327
+ "reason": "not inside a git worktree",
328
+ "path": str(path),
329
+ },
330
+ as_json=as_json,
331
+ )
332
+ raise typer.Exit(code=0)
333
+
292
334
  slug = project or detect_project_slug(path)
293
335
  pipe = Pipeline(project=slug)
294
336
  ex = pipe.reingest_file(path)