threadkeeper 0.6.0__tar.gz → 0.6.1__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 (103) hide show
  1. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/PKG-INFO +2 -1
  2. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/README.md +1 -0
  3. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/pyproject.toml +1 -1
  4. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_memory_guard.py +37 -1
  5. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/config.py +3 -0
  6. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/memory_guard.py +4 -0
  7. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/memory_guard.py +1 -0
  8. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/PKG-INFO +2 -1
  9. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/LICENSE +0 -0
  10. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/setup.cfg +0 -0
  11. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_adapters.py +0 -0
  12. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_brief_sections.py +0 -0
  13. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_candidate_reviewer.py +0 -0
  14. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_core_memory.py +0 -0
  15. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_curator.py +0 -0
  16. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_delegated_search.py +0 -0
  17. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_dialectic.py +0 -0
  18. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_dialectic_tier.py +0 -0
  19. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_error_paths.py +0 -0
  20. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_extract_daemon.py +0 -0
  21. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_i18n_multilang.py +0 -0
  22. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_identity.py +0 -0
  23. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_lessons.py +0 -0
  24. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_missed_spawns.py +0 -0
  25. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_nudges.py +0 -0
  26. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_process_health.py +0 -0
  27. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_shadow_review.py +0 -0
  28. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_skill_hint.py +0 -0
  29. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_skill_tier.py +0 -0
  30. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_skill_use_parser.py +0 -0
  31. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_skill_watcher.py +0 -0
  32. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_skills.py +0 -0
  33. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_spawn_budget.py +0 -0
  34. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_spawn_config.py +0 -0
  35. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_spawn_hint.py +0 -0
  36. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_spawn_slim.py +0 -0
  37. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_threads.py +0 -0
  38. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_tools_smoke.py +0 -0
  39. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_validate_threads.py +0 -0
  40. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/tests/test_vec_search.py +0 -0
  41. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/__init__.py +0 -0
  42. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/_mcp.py +0 -0
  43. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/_setup.py +0 -0
  44. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/__init__.py +0 -0
  45. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/_hook_helpers.py +0 -0
  46. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/base.py +0 -0
  47. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/claude_code.py +0 -0
  48. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/claude_desktop.py +0 -0
  49. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/codex.py +0 -0
  50. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/copilot.py +0 -0
  51. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/gemini.py +0 -0
  52. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/adapters/vscode.py +0 -0
  53. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/brief.py +0 -0
  54. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/candidate_reviewer.py +0 -0
  55. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/curator.py +0 -0
  56. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/db.py +0 -0
  57. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/embeddings.py +0 -0
  58. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/extract_daemon.py +0 -0
  59. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/helpers.py +0 -0
  60. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/i18n.py +0 -0
  61. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/identity.py +0 -0
  62. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/ingest.py +0 -0
  63. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/lessons.py +0 -0
  64. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/nudges.py +0 -0
  65. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/process_health.py +0 -0
  66. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/review_prompts.py +0 -0
  67. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/search_proxy.py +0 -0
  68. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/server.py +0 -0
  69. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/shadow_review.py +0 -0
  70. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/skill_watcher.py +0 -0
  71. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/spawn_budget.py +0 -0
  72. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/spawn_config.py +0 -0
  73. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/__init__.py +0 -0
  74. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/candidate_reviewer.py +0 -0
  75. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/concepts.py +0 -0
  76. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/consolidate.py +0 -0
  77. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/core_memory.py +0 -0
  78. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/correlation.py +0 -0
  79. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/curator.py +0 -0
  80. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/dialectic.py +0 -0
  81. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/dialog.py +0 -0
  82. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/distill.py +0 -0
  83. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/extract.py +0 -0
  84. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/graph.py +0 -0
  85. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/invariants.py +0 -0
  86. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/lessons.py +0 -0
  87. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/missed_spawns.py +0 -0
  88. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/peers.py +0 -0
  89. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/pickup.py +0 -0
  90. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/probes.py +0 -0
  91. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/process_health.py +0 -0
  92. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/session.py +0 -0
  93. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/shadow_review.py +0 -0
  94. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/skills.py +0 -0
  95. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/spawn.py +0 -0
  96. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/style.py +0 -0
  97. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/threads.py +0 -0
  98. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper/tools/validate.py +0 -0
  99. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/SOURCES.txt +0 -0
  100. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/dependency_links.txt +0 -0
  101. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/entry_points.txt +0 -0
  102. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/requires.txt +0 -0
  103. {threadkeeper-0.6.0 → threadkeeper-0.6.1}/threadkeeper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, Gemini, Copilot, VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server.
5
5
  Author: thread-keeper contributors
6
6
  License: MIT
@@ -432,6 +432,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
432
432
  | `THREADKEEPER_MEMORY_GUARD_RECLAIM_MB` | 1024 | local RSS floor before warn-triggered self trim |
433
433
  | `THREADKEEPER_MEMORY_GUARD_TARGET_SERVERS` | 1 | aggregate-pressure target after retiring stale idle servers |
434
434
  | `THREADKEEPER_MEMORY_GUARD_RETIRE_IDLE_S` | 900 | heartbeat age before a non-self server is retireable |
435
+ | `THREADKEEPER_MEMORY_GUARD_RETIRE_LIVE` | "" (off) | allow retiring parent-alive MCP servers; off protects live clients |
435
436
  | `THREADKEEPER_MEMORY_GUARD_NOTIFY` | "1" | send macOS desktop notification when possible |
436
437
  | `THREADKEEPER_INGEST_INTERVAL_S` | 3 | transcript ingest tick (s) |
437
438
  | `THREADKEEPER_NO_EMBEDDINGS` | "" | force-disable sentence-transformers |
@@ -398,6 +398,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
398
398
  | `THREADKEEPER_MEMORY_GUARD_RECLAIM_MB` | 1024 | local RSS floor before warn-triggered self trim |
399
399
  | `THREADKEEPER_MEMORY_GUARD_TARGET_SERVERS` | 1 | aggregate-pressure target after retiring stale idle servers |
400
400
  | `THREADKEEPER_MEMORY_GUARD_RETIRE_IDLE_S` | 900 | heartbeat age before a non-self server is retireable |
401
+ | `THREADKEEPER_MEMORY_GUARD_RETIRE_LIVE` | "" (off) | allow retiring parent-alive MCP servers; off protects live clients |
401
402
  | `THREADKEEPER_MEMORY_GUARD_NOTIFY` | "1" | send macOS desktop notification when possible |
402
403
  | `THREADKEEPER_INGEST_INTERVAL_S` | 3 | transcript ingest tick (s) |
403
404
  | `THREADKEEPER_NO_EMBEDDINGS` | "" | force-disable sentence-transformers |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "Multi-agent shared brain across Claude Code/Desktop, Codex, Gemini, Copilot, VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server."
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "thread-keeper contributors" }]
@@ -195,6 +195,7 @@ def test_check_apply_retires_idle_candidate_on_aggregate_pressure(mp_with_cid, m
195
195
  monkeypatch.setattr(memory_guard, "MEMORY_GUARD_AGG_KILL_MB", 3000)
196
196
  monkeypatch.setattr(memory_guard, "MEMORY_GUARD_TARGET_SERVERS", 1)
197
197
  monkeypatch.setattr(memory_guard, "MEMORY_GUARD_RETIRE_IDLE_S", 900)
198
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_RETIRE_LIVE", False)
198
199
  monkeypatch.setattr(memory_guard, "reclaim_memory", lambda reason="": {
199
200
  "before_mb": 900, "after_mb": 800, "freed_mb": 100,
200
201
  "pid": os.getpid(), "actions": ["fake"],
@@ -203,7 +204,12 @@ def test_check_apply_retires_idle_candidate_on_aggregate_pressure(mp_with_cid, m
203
204
  def scan():
204
205
  return [
205
206
  _proc(os.getpid(), 900),
206
- _proc(1001, 1200, ppid=os.getpid()) | {"heartbeat_age_s": None},
207
+ _proc(1001, 1200, ppid=1) | {
208
+ "heartbeat_age_s": None,
209
+ "parent_alive": False,
210
+ "is_orphaned": True,
211
+ "orphan_reason": "parent_gone + no_heartbeat",
212
+ },
207
213
  _proc(1002, 800, ppid=os.getpid()) | {"heartbeat_age_s": 5},
208
214
  ]
209
215
 
@@ -217,3 +223,33 @@ def test_check_apply_retires_idle_candidate_on_aggregate_pressure(mp_with_cid, m
217
223
  out = memory_guard.check_once(dry_run=False, notify=False)
218
224
  assert out["retired"] == [1001]
219
225
  assert calls == [(1001, _sig.SIGTERM)]
226
+
227
+
228
+ def test_aggregate_retire_skips_live_parent_without_opt_in(mp_with_cid, monkeypatch):
229
+ mp_with_cid(_FAKE_CID)
230
+ from threadkeeper import memory_guard, process_health
231
+
232
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_WARN_MB", 5000)
233
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_KILL_MB", 6000)
234
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_AGG_WARN_MB", 2000)
235
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_AGG_KILL_MB", 3000)
236
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_TARGET_SERVERS", 1)
237
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_RETIRE_IDLE_S", 900)
238
+ monkeypatch.setattr(memory_guard, "MEMORY_GUARD_RETIRE_LIVE", False)
239
+ monkeypatch.setattr(memory_guard, "reclaim_memory", lambda reason="": {
240
+ "before_mb": 900, "after_mb": 800, "freed_mb": 100,
241
+ "pid": os.getpid(), "actions": ["fake"],
242
+ })
243
+ monkeypatch.setattr(process_health, "scan", lambda: [
244
+ _proc(os.getpid(), 900),
245
+ _proc(1001, 1200, ppid=os.getpid()) | {"heartbeat_age_s": None},
246
+ ])
247
+ calls: list[tuple[int, int]] = []
248
+ monkeypatch.setattr(
249
+ "os.kill",
250
+ lambda pid, sig: calls.append((pid, sig)) if sig != 0 else None,
251
+ )
252
+
253
+ out = memory_guard.check_once(dry_run=False, notify=False)
254
+ assert out["retired"] == []
255
+ assert calls == []
@@ -183,6 +183,9 @@ MEMORY_GUARD_TARGET_SERVERS: int = int(
183
183
  MEMORY_GUARD_RETIRE_IDLE_S: int = int(
184
184
  os.environ.get("THREADKEEPER_MEMORY_GUARD_RETIRE_IDLE_S", "900")
185
185
  )
186
+ MEMORY_GUARD_RETIRE_LIVE: bool = os.environ.get(
187
+ "THREADKEEPER_MEMORY_GUARD_RETIRE_LIVE", ""
188
+ ).lower() in {"1", "true", "yes", "on"}
186
189
  MEMORY_GUARD_NOTIFY: bool = os.environ.get(
187
190
  "THREADKEEPER_MEMORY_GUARD_NOTIFY", "1"
188
191
  ).lower() in {"1", "true", "yes", "on"}
@@ -28,6 +28,7 @@ from .config import (
28
28
  MEMORY_GUARD_POLL_S,
29
29
  MEMORY_GUARD_RECLAIM_MB,
30
30
  MEMORY_GUARD_RETIRE_IDLE_S,
31
+ MEMORY_GUARD_RETIRE_LIVE,
31
32
  MEMORY_GUARD_TARGET_SERVERS,
32
33
  MEMORY_GUARD_WARN_MB,
33
34
  TASK_LOG_DIR,
@@ -288,6 +289,7 @@ def _aggregate_state(procs: list[dict]) -> dict:
288
289
  "kill_mb": MEMORY_GUARD_AGG_KILL_MB,
289
290
  "target_servers": MEMORY_GUARD_TARGET_SERVERS,
290
291
  "retire_idle_s": MEMORY_GUARD_RETIRE_IDLE_S,
292
+ "retire_live": MEMORY_GUARD_RETIRE_LIVE,
291
293
  }
292
294
 
293
295
 
@@ -296,6 +298,8 @@ def _idle_retire_candidates(procs: list[dict]) -> list[dict]:
296
298
  for p in procs:
297
299
  if p.get("is_self"):
298
300
  continue
301
+ if p.get("parent_alive") and not MEMORY_GUARD_RETIRE_LIVE:
302
+ continue
299
303
  hb = p.get("heartbeat_age_s")
300
304
  if hb is None or hb >= MEMORY_GUARD_RETIRE_IDLE_S:
301
305
  candidates.append(dict(p, rss_mb=_rss_mb(p)))
@@ -32,6 +32,7 @@ def memory_guard_status() -> str:
32
32
  f"warn_mb={result['warn_mb']} kill_mb={result['kill_mb']} "
33
33
  f"agg_warn_mb={agg['warn_mb']} agg_kill_mb={agg['kill_mb']} "
34
34
  f"target_servers={agg['target_servers']} "
35
+ f"retire_live={'on' if agg['retire_live'] else 'off'} "
35
36
  f"notify={'on' if result['notify'] else 'off'}",
36
37
  f"processes={len(procs)} rss_total={total_mb}MB aggregate={agg_marker}",
37
38
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, Gemini, Copilot, VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server.
5
5
  Author: thread-keeper contributors
6
6
  License: MIT
@@ -432,6 +432,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
432
432
  | `THREADKEEPER_MEMORY_GUARD_RECLAIM_MB` | 1024 | local RSS floor before warn-triggered self trim |
433
433
  | `THREADKEEPER_MEMORY_GUARD_TARGET_SERVERS` | 1 | aggregate-pressure target after retiring stale idle servers |
434
434
  | `THREADKEEPER_MEMORY_GUARD_RETIRE_IDLE_S` | 900 | heartbeat age before a non-self server is retireable |
435
+ | `THREADKEEPER_MEMORY_GUARD_RETIRE_LIVE` | "" (off) | allow retiring parent-alive MCP servers; off protects live clients |
435
436
  | `THREADKEEPER_MEMORY_GUARD_NOTIFY` | "1" | send macOS desktop notification when possible |
436
437
  | `THREADKEEPER_INGEST_INTERVAL_S` | 3 | transcript ingest tick (s) |
437
438
  | `THREADKEEPER_NO_EMBEDDINGS` | "" | force-disable sentence-transformers |
File without changes
File without changes