threadkeeper 0.9.1__tar.gz → 0.10.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 (151) hide show
  1. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/PKG-INFO +54 -11
  2. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/README.md +53 -10
  3. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/pyproject.toml +10 -1
  4. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_agent_status.py +141 -0
  5. threadkeeper-0.10.0/tests/test_auto_update.py +138 -0
  6. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_config_settings.py +5 -0
  7. threadkeeper-0.10.0/tests/test_menubar_app.py +186 -0
  8. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_thread_janitor.py +1 -0
  9. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/agent_status.py +127 -0
  10. threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/Info.plist +25 -0
  11. threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/README.md +83 -0
  12. threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +760 -0
  13. threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/build.sh +27 -0
  14. threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/install.sh +63 -0
  15. threadkeeper-0.10.0/threadkeeper/auto_update.py +347 -0
  16. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/config.py +8 -0
  17. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/identity.py +5 -0
  18. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/menubar_app.py +101 -5
  19. threadkeeper-0.10.0/threadkeeper/tools/agent_status.py +41 -0
  20. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dashboard.py +1 -0
  21. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/PKG-INFO +54 -11
  22. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/SOURCES.txt +8 -0
  23. threadkeeper-0.9.1/threadkeeper/tools/agent_status.py +0 -19
  24. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/LICENSE +0 -0
  25. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/setup.cfg +0 -0
  26. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_adapters.py +0 -0
  27. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_brief_footprint.py +0 -0
  28. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_brief_sections.py +0 -0
  29. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_candidate_reviewer.py +0 -0
  30. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_core_memory.py +0 -0
  31. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_curator.py +0 -0
  32. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dashboard.py +0 -0
  33. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_delegated_search.py +0 -0
  34. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic.py +0 -0
  35. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_feed_tools.py +0 -0
  36. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_miner.py +0 -0
  37. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_observation_resolve.py +0 -0
  38. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_recompute.py +0 -0
  39. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_tier.py +0 -0
  40. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_validator.py +0 -0
  41. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_error_paths.py +0 -0
  42. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_applier.py +0 -0
  43. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_apply_2.py +0 -0
  44. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_apply_3.py +0 -0
  45. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_daemon.py +0 -0
  46. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_extract_daemon.py +0 -0
  47. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_extract_dedup.py +0 -0
  48. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_i18n_multilang.py +0 -0
  49. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_identity.py +0 -0
  50. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_ingest_status.py +0 -0
  51. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_lessons.py +0 -0
  52. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_memory_guard.py +0 -0
  53. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_missed_spawns.py +0 -0
  54. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_nudges.py +0 -0
  55. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_onnx_embeddings.py +0 -0
  56. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_panel.py +0 -0
  57. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_probe_daemon.py +0 -0
  58. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_process_health.py +0 -0
  59. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_search_fts_punctuation.py +0 -0
  60. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_shadow_review.py +0 -0
  61. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_hint.py +0 -0
  62. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_passive_tier.py +0 -0
  63. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_tier.py +0 -0
  64. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_use_parser.py +0 -0
  65. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_watcher.py +0 -0
  66. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skills.py +0 -0
  67. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_budget.py +0 -0
  68. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_codex_stdin.py +0 -0
  69. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_config.py +0 -0
  70. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_hint.py +0 -0
  71. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_reap.py +0 -0
  72. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_slim.py +0 -0
  73. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_wrap.py +0 -0
  74. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_threads.py +0 -0
  75. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_tools_smoke.py +0 -0
  76. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_validate_threads.py +0 -0
  77. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_vec_search.py +0 -0
  78. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/__init__.py +0 -0
  79. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_mcp.py +0 -0
  80. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_setup.py +0 -0
  81. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_spawn_wrap.py +0 -0
  82. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/__init__.py +0 -0
  83. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  84. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/base.py +0 -0
  85. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_code.py +0 -0
  86. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  87. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/codex.py +0 -0
  88. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/copilot.py +0 -0
  89. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/gemini.py +0 -0
  90. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/vscode.py +0 -0
  91. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/brief.py +0 -0
  92. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/candidate_reviewer.py +0 -0
  93. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/curator.py +0 -0
  94. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/db.py +0 -0
  95. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/dialectic_miner.py +0 -0
  96. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/dialectic_validator.py +0 -0
  97. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/embeddings.py +0 -0
  98. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/evolve_applier.py +0 -0
  99. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/evolve_daemon.py +0 -0
  100. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/extract_daemon.py +0 -0
  101. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/helpers.py +0 -0
  102. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/i18n.py +0 -0
  103. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/ingest.py +0 -0
  104. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/lessons.py +0 -0
  105. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/memory_guard.py +0 -0
  106. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/migrate_embeddings.py +0 -0
  107. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/nudges.py +0 -0
  108. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/probe_daemon.py +0 -0
  109. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/process_health.py +0 -0
  110. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/review_prompts.py +0 -0
  111. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/search_proxy.py +0 -0
  112. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/server.py +0 -0
  113. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/shadow_review.py +0 -0
  114. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/skill_watcher.py +0 -0
  115. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/spawn_budget.py +0 -0
  116. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/spawn_config.py +0 -0
  117. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/thread_janitor.py +0 -0
  118. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/__init__.py +0 -0
  119. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  120. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/concepts.py +0 -0
  121. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/consolidate.py +0 -0
  122. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/core_memory.py +0 -0
  123. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/correlation.py +0 -0
  124. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/curator.py +0 -0
  125. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic.py +0 -0
  126. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  127. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialog.py +0 -0
  128. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/distill.py +0 -0
  129. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/evolve_applier.py +0 -0
  130. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/extract.py +0 -0
  131. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/graph.py +0 -0
  132. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/invariants.py +0 -0
  133. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/lessons.py +0 -0
  134. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/memory_guard.py +0 -0
  135. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/missed_spawns.py +0 -0
  136. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/panel.py +0 -0
  137. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/peers.py +0 -0
  138. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/pickup.py +0 -0
  139. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/probes.py +0 -0
  140. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/process_health.py +0 -0
  141. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/session.py +0 -0
  142. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/shadow_review.py +0 -0
  143. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/skills.py +0 -0
  144. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/spawn.py +0 -0
  145. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/style.py +0 -0
  146. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/threads.py +0 -0
  147. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/validate.py +0 -0
  148. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  149. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/entry_points.txt +0 -0
  150. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/requires.txt +0 -0
  151. {threadkeeper-0.9.1 → threadkeeper-0.10.0}/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.9.1
3
+ Version: 0.10.0
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
@@ -96,6 +96,12 @@ make it more than a memory store:
96
96
  `THREADKEEPER_EXTRA_SKILLS_DIRS`, and `~/.threadkeeper/skills/`),
97
97
  with lessons.md as a fallback for CLIs without a native skills loader.
98
98
 
99
+ Foreground MCP servers also run a daily self-update check by default. Source
100
+ checkouts fast-forward their tracked git branch and reinstall the editable
101
+ package; PyPI/pipx/venv installs run `pip install --upgrade` in the current
102
+ interpreter environment. Dirty or diverged git checkouts are skipped rather
103
+ than overwritten.
104
+
99
105
  ---
100
106
 
101
107
  ## Quickstart
@@ -206,23 +212,56 @@ or compact text for external monitors:
206
212
  ```sh
207
213
  tk-agent-status
208
214
  tk-agent-status --json
215
+ tk-agent-status --cleanup-memory
209
216
  ```
210
217
 
211
218
  `apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
212
219
  command every 5 seconds and shows every autonomous learning loop: enabled/off,
213
220
  running/idle/ready, last pass, backlog, and active child RSS when that loop has
214
- spawned a worker. Active loops are sorted first (`running`, then `ready`), so
215
- background work stays at the top of the panel. The app also requests macOS
216
- notification permission and sends a notification when a newly completed
221
+ spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
222
+ `threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
223
+ does not need a git checkout for the widget to build. Active loops are sorted
224
+ first (`running`, then `ready`), so background work stays at the top of the
225
+ panel. `tk-agent-status --cleanup-memory` runs the safe cleanup path used by the
226
+ widget: request server cache trims, apply the RSS guard, and remove orphan MCP
227
+ server processes without killing active spawned child agents. The menu-bar
228
+ status item is backed by AppKit `NSStatusItem`: it shows the black `memorychip`
229
+ icon while idle, then swaps fixed-center, synchronized gear frames whenever
230
+ `running_loop_count` reports at least one active autonomous loop. The status item is
231
+ icon-only; loop counts live in the popover and tooltip. The app also has a Clean
232
+ memory button, self-restarts when its own RSS crosses
233
+ `THREADKEEPER_MENUBAR_RESTART_RSS_MB` (1024 MB default), requests macOS
234
+ notification permission, and sends a notification when a newly completed
217
235
  autonomous child task produces a useful result in `recent_results`; the first
218
236
  poll only marks existing results as seen, so old completions do not spam
219
- notifications. Probe backlog is due objective probes only, not every registered
220
- probe, so a healthy cooldown shows `0 due probes` instead of looking stuck. On
221
- macOS, `python -m threadkeeper.server` automatically installs and launches it
222
- on MCP startup. Set `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that
223
- behavior.
224
-
225
- Manual fallback:
237
+ notifications. Probe backlog is due objective
238
+ probes only, not every registered probe, so a healthy cooldown shows `0 due
239
+ probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
240
+ automatically installs and launches it on MCP startup, and restarts the app when
241
+ the installed bundle has changed while an older menu-bar process is still
242
+ running. Set
243
+ `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
244
+
245
+ ### Auto Update
246
+
247
+ The MCP server starts an auto-update daemon in foreground parent processes.
248
+ By default it checks once per day (`THREADKEEPER_AUTO_UPDATE_INTERVAL_S=86400`):
249
+
250
+ - editable git checkout: skip if tracked files are dirty, otherwise fetch the
251
+ tracked remote branch, fast-forward with `git pull --ff-only`, reinstall the
252
+ editable package, and rerun `threadkeeper._setup`;
253
+ - installed package: run `pip install --upgrade threadkeeper` or
254
+ `threadkeeper[semantic]` in the current interpreter environment, preserving
255
+ semantic extras when they are already installed, then rerun setup when the
256
+ installed version changes.
257
+
258
+ After a successful update, the daemon exits the current MCP process by default
259
+ so the host can restart it on the new code. Disable that with
260
+ `THREADKEEPER_AUTO_UPDATE_RESTART=0`, or disable the updater entirely with
261
+ `THREADKEEPER_AUTO_UPDATE_INTERVAL_S=0`. Each real check records an
262
+ `auto_update_pass` event that appears in dashboard/status telemetry.
263
+
264
+ Manual fallback from a source checkout:
226
265
 
227
266
  ```sh
228
267
  cd apps/macos-agent-status
@@ -510,6 +549,9 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
510
549
  |---|---|---|
511
550
  | `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
512
551
  | `THREADKEEPER_AUTO_REVIEW` | "" (off) | auto-review on `close_thread` |
552
+ | `THREADKEEPER_AUTO_UPDATE_INTERVAL_S` | 86400 | MCP self-update check interval; 0 disables |
553
+ | `THREADKEEPER_AUTO_UPDATE_RESTART` | "1" | exit MCP process after applying an update so the host restarts on new code |
554
+ | `THREADKEEPER_AUTO_UPDATE_TIMEOUT_S` | 600 | max seconds for git/pip update commands |
513
555
  | `THREADKEEPER_SHADOW_REVIEW_INTERVAL_S` | 0 (off) | shadow daemon tick (s) |
514
556
  | `THREADKEEPER_SHADOW_REVIEW_WINDOW_S` | 900 | sliding window for shadow scan (s) |
515
557
  | `THREADKEEPER_EXTRACT_INTERVAL_S` | 0 (off) | extract daemon tick (s); 600 = 10 min recommended |
@@ -523,6 +565,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
523
565
  | `THREADKEEPER_PROBE_COOLDOWN_S` | 604800 | per-category probe cooldown; 86400 = 1d recommended for active reliability tracking |
524
566
  | `THREADKEEPER_SPAWN_BUDGET_MB` | 3072 | combined child RSS cap (MB); 0 disables |
525
567
  | `THREADKEEPER_MENUBAR_AUTO_LAUNCH` | true | macOS: auto install/launch status menu-bar app on MCP startup |
568
+ | `THREADKEEPER_MENUBAR_RESTART_RSS_MB` | 1024 | macOS widget self-restart RSS threshold; 0 disables |
526
569
  | `THREADKEEPER_MEMORY_GUARD_POLL_S` | 30 | server RSS guard tick (s); 0 disables |
527
570
  | `THREADKEEPER_MEMORY_GUARD_WARN_MB` | 1536 | notify/log when a server crosses this RSS |
528
571
  | `THREADKEEPER_MEMORY_GUARD_KILL_MB` | 3072 | SIGTERM server above this RSS; 0 disables killing |
@@ -55,6 +55,12 @@ make it more than a memory store:
55
55
  `THREADKEEPER_EXTRA_SKILLS_DIRS`, and `~/.threadkeeper/skills/`),
56
56
  with lessons.md as a fallback for CLIs without a native skills loader.
57
57
 
58
+ Foreground MCP servers also run a daily self-update check by default. Source
59
+ checkouts fast-forward their tracked git branch and reinstall the editable
60
+ package; PyPI/pipx/venv installs run `pip install --upgrade` in the current
61
+ interpreter environment. Dirty or diverged git checkouts are skipped rather
62
+ than overwritten.
63
+
58
64
  ---
59
65
 
60
66
  ## Quickstart
@@ -165,23 +171,56 @@ or compact text for external monitors:
165
171
  ```sh
166
172
  tk-agent-status
167
173
  tk-agent-status --json
174
+ tk-agent-status --cleanup-memory
168
175
  ```
169
176
 
170
177
  `apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
171
178
  command every 5 seconds and shows every autonomous learning loop: enabled/off,
172
179
  running/idle/ready, last pass, backlog, and active child RSS when that loop has
173
- spawned a worker. Active loops are sorted first (`running`, then `ready`), so
174
- background work stays at the top of the panel. The app also requests macOS
175
- notification permission and sends a notification when a newly completed
180
+ spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
181
+ `threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
182
+ does not need a git checkout for the widget to build. Active loops are sorted
183
+ first (`running`, then `ready`), so background work stays at the top of the
184
+ panel. `tk-agent-status --cleanup-memory` runs the safe cleanup path used by the
185
+ widget: request server cache trims, apply the RSS guard, and remove orphan MCP
186
+ server processes without killing active spawned child agents. The menu-bar
187
+ status item is backed by AppKit `NSStatusItem`: it shows the black `memorychip`
188
+ icon while idle, then swaps fixed-center, synchronized gear frames whenever
189
+ `running_loop_count` reports at least one active autonomous loop. The status item is
190
+ icon-only; loop counts live in the popover and tooltip. The app also has a Clean
191
+ memory button, self-restarts when its own RSS crosses
192
+ `THREADKEEPER_MENUBAR_RESTART_RSS_MB` (1024 MB default), requests macOS
193
+ notification permission, and sends a notification when a newly completed
176
194
  autonomous child task produces a useful result in `recent_results`; the first
177
195
  poll only marks existing results as seen, so old completions do not spam
178
- notifications. Probe backlog is due objective probes only, not every registered
179
- probe, so a healthy cooldown shows `0 due probes` instead of looking stuck. On
180
- macOS, `python -m threadkeeper.server` automatically installs and launches it
181
- on MCP startup. Set `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that
182
- behavior.
183
-
184
- Manual fallback:
196
+ notifications. Probe backlog is due objective
197
+ probes only, not every registered probe, so a healthy cooldown shows `0 due
198
+ probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
199
+ automatically installs and launches it on MCP startup, and restarts the app when
200
+ the installed bundle has changed while an older menu-bar process is still
201
+ running. Set
202
+ `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
203
+
204
+ ### Auto Update
205
+
206
+ The MCP server starts an auto-update daemon in foreground parent processes.
207
+ By default it checks once per day (`THREADKEEPER_AUTO_UPDATE_INTERVAL_S=86400`):
208
+
209
+ - editable git checkout: skip if tracked files are dirty, otherwise fetch the
210
+ tracked remote branch, fast-forward with `git pull --ff-only`, reinstall the
211
+ editable package, and rerun `threadkeeper._setup`;
212
+ - installed package: run `pip install --upgrade threadkeeper` or
213
+ `threadkeeper[semantic]` in the current interpreter environment, preserving
214
+ semantic extras when they are already installed, then rerun setup when the
215
+ installed version changes.
216
+
217
+ After a successful update, the daemon exits the current MCP process by default
218
+ so the host can restart it on the new code. Disable that with
219
+ `THREADKEEPER_AUTO_UPDATE_RESTART=0`, or disable the updater entirely with
220
+ `THREADKEEPER_AUTO_UPDATE_INTERVAL_S=0`. Each real check records an
221
+ `auto_update_pass` event that appears in dashboard/status telemetry.
222
+
223
+ Manual fallback from a source checkout:
185
224
 
186
225
  ```sh
187
226
  cd apps/macos-agent-status
@@ -469,6 +508,9 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
469
508
  |---|---|---|
470
509
  | `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
471
510
  | `THREADKEEPER_AUTO_REVIEW` | "" (off) | auto-review on `close_thread` |
511
+ | `THREADKEEPER_AUTO_UPDATE_INTERVAL_S` | 86400 | MCP self-update check interval; 0 disables |
512
+ | `THREADKEEPER_AUTO_UPDATE_RESTART` | "1" | exit MCP process after applying an update so the host restarts on new code |
513
+ | `THREADKEEPER_AUTO_UPDATE_TIMEOUT_S` | 600 | max seconds for git/pip update commands |
472
514
  | `THREADKEEPER_SHADOW_REVIEW_INTERVAL_S` | 0 (off) | shadow daemon tick (s) |
473
515
  | `THREADKEEPER_SHADOW_REVIEW_WINDOW_S` | 900 | sliding window for shadow scan (s) |
474
516
  | `THREADKEEPER_EXTRACT_INTERVAL_S` | 0 (off) | extract daemon tick (s); 600 = 10 min recommended |
@@ -482,6 +524,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
482
524
  | `THREADKEEPER_PROBE_COOLDOWN_S` | 604800 | per-category probe cooldown; 86400 = 1d recommended for active reliability tracking |
483
525
  | `THREADKEEPER_SPAWN_BUDGET_MB` | 3072 | combined child RSS cap (MB); 0 disables |
484
526
  | `THREADKEEPER_MENUBAR_AUTO_LAUNCH` | true | macOS: auto install/launch status menu-bar app on MCP startup |
527
+ | `THREADKEEPER_MENUBAR_RESTART_RSS_MB` | 1024 | macOS widget self-restart RSS threshold; 0 disables |
485
528
  | `THREADKEEPER_MEMORY_GUARD_POLL_S` | 30 | server RSS guard tick (s); 0 disables |
486
529
  | `THREADKEEPER_MEMORY_GUARD_WARN_MB` | 1536 | notify/log when a server crosses this RSS |
487
530
  | `THREADKEEPER_MEMORY_GUARD_KILL_MB` | 3072 | SIGTERM server above this RSS; 0 disables killing |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.9.1"
7
+ version = "0.10.0"
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" }]
@@ -78,6 +78,15 @@ tk-agent-status = "threadkeeper.agent_status:main"
78
78
  include = ["threadkeeper*"]
79
79
  exclude = ["tests*", "scripts*"]
80
80
 
81
+ [tool.setuptools.package-data]
82
+ threadkeeper = [
83
+ "assets/macos-agent-status/Info.plist",
84
+ "assets/macos-agent-status/README.md",
85
+ "assets/macos-agent-status/ThreadKeeperAgentStatus.swift",
86
+ "assets/macos-agent-status/build.sh",
87
+ "assets/macos-agent-status/install.sh",
88
+ ]
89
+
81
90
  [tool.pytest.ini_options]
82
91
  testpaths = ["tests"]
83
92
  addopts = "-q --strict-markers"
@@ -87,6 +87,8 @@ def test_agent_status_snapshot_reports_running_agents(mp_with_cid):
87
87
  assert by_id["tk_generic"]["name"] == "child-tk"
88
88
  assert by_id["tk_generic"]["description"].startswith("Spawned child task")
89
89
  by_loop = {loop["id"]: loop for loop in snap["loops"]}
90
+ assert "auto_update" in by_loop
91
+ assert "daily updates" in by_loop["auto_update"]["description"]
90
92
  assert "Reviews extracted conversation candidates" in by_loop[
91
93
  "candidate_reviewer"
92
94
  ]["description"]
@@ -273,6 +275,145 @@ def test_agent_status_mcp_json_output(mp_with_cid):
273
275
  assert data["agents"][0]["rss_mb"] == 100
274
276
 
275
277
 
278
+ def test_agent_memory_cleanup_runs_guard_and_orphan_cleanup(mp_with_cid, monkeypatch):
279
+ pkg = mp_with_cid(_FAKE_CID)
280
+ from threadkeeper import agent_status, memory_guard, process_health
281
+
282
+ _insert_task(pkg, "tk_status", "Build a compact menu-bar status app.", rss_mb=100)
283
+ calls = []
284
+ monkeypatch.setattr(agent_status, "_refresh_rss", lambda conn: calls.append("refresh"))
285
+ monkeypatch.setattr(
286
+ memory_guard,
287
+ "request_reclaim",
288
+ lambda reason: calls.append(("trim", reason)) or {
289
+ "requested": [11, 22],
290
+ "count": 2,
291
+ "reason": reason,
292
+ },
293
+ )
294
+ monkeypatch.setattr(
295
+ memory_guard,
296
+ "check_once",
297
+ lambda dry_run, notify: calls.append(("guard", dry_run, notify)) or {
298
+ "warn": [{}],
299
+ "kill": [{}],
300
+ "killed": [33],
301
+ "retired": [44],
302
+ "failed": [],
303
+ "aggregate": {"warn": True, "rss_mb": 2048},
304
+ "reclaim_requests": {"count": 1, "requested": [55]},
305
+ "local_reclaim": {
306
+ "before_mb": 1500,
307
+ "after_mb": 900,
308
+ "freed_mb": 600,
309
+ },
310
+ "handled_controls": [],
311
+ },
312
+ )
313
+ monkeypatch.setattr(
314
+ process_health,
315
+ "cleanup",
316
+ lambda dry_run, force: calls.append(("orphans", dry_run, force)) or {
317
+ "orphans": [{"pid": 66}],
318
+ "killed": [66],
319
+ "failed": [],
320
+ },
321
+ )
322
+
323
+ result = agent_status.memory_cleanup(dry_run=False, force=True)
324
+ text = agent_status.format_memory_cleanup(result)
325
+
326
+ assert result["peer_trim_requested"]["count"] == 2
327
+ assert result["guard"]["killed"] == [33]
328
+ assert result["guard"]["retired"] == [44]
329
+ assert result["orphans"]["killed"] == [66]
330
+ assert ("guard", False, False) in calls
331
+ assert ("orphans", False, True) in calls
332
+ assert "local_reclaim before=1500MB after=900MB freed=600MB" in text
333
+
334
+
335
+ def test_agent_memory_cleanup_dry_run_does_not_request_peer_trim(
336
+ mp_with_cid,
337
+ monkeypatch,
338
+ ):
339
+ mp_with_cid(_FAKE_CID)
340
+ from threadkeeper import agent_status, memory_guard, process_health
341
+
342
+ calls = []
343
+ monkeypatch.setattr(agent_status, "_refresh_rss", lambda conn: None)
344
+ monkeypatch.setattr(
345
+ memory_guard,
346
+ "request_reclaim",
347
+ lambda reason: calls.append(("trim", reason)) or {
348
+ "requested": [11],
349
+ "count": 1,
350
+ "reason": reason,
351
+ },
352
+ )
353
+ monkeypatch.setattr(
354
+ memory_guard,
355
+ "check_once",
356
+ lambda dry_run, notify: {
357
+ "warn": [],
358
+ "kill": [],
359
+ "killed": [],
360
+ "retired": [],
361
+ "failed": [],
362
+ "aggregate": {},
363
+ "reclaim_requests": {},
364
+ "local_reclaim": None,
365
+ "handled_controls": [],
366
+ },
367
+ )
368
+ monkeypatch.setattr(
369
+ process_health,
370
+ "cleanup",
371
+ lambda dry_run, force: {"orphans": [], "killed": [], "failed": []},
372
+ )
373
+
374
+ result = agent_status.memory_cleanup(dry_run=True)
375
+
376
+ assert result["dry_run"] is True
377
+ assert result["peer_trim_requested"]["count"] == 0
378
+ assert calls == []
379
+
380
+
381
+ def test_agent_memory_cleanup_tool_json_output(mp_with_cid, monkeypatch):
382
+ pkg = mp_with_cid(_FAKE_CID)
383
+ from threadkeeper import agent_status as agent_status_mod
384
+
385
+ monkeypatch.setattr(
386
+ "threadkeeper.tools.agent_status.memory_cleanup",
387
+ lambda dry_run, force: {
388
+ "dry_run": dry_run,
389
+ "force": force,
390
+ "before": {"running_count": 0, "child_rss_mb": 0},
391
+ "after": {"running_count": 0, "child_rss_mb": 0},
392
+ "peer_trim_requested": {"requested": [], "count": 0},
393
+ "guard": {
394
+ "warn": 0,
395
+ "kill": 0,
396
+ "killed": [],
397
+ "retired": [],
398
+ "failed": [],
399
+ "local_reclaim": None,
400
+ },
401
+ "orphans": {"count": 0, "killed": [], "failed": []},
402
+ },
403
+ )
404
+
405
+ raw = _tool(pkg, "agent_memory_cleanup")(
406
+ json_output=True,
407
+ dry_run=True,
408
+ force=True,
409
+ )
410
+ data = json.loads(raw)
411
+
412
+ assert data["dry_run"] is True
413
+ assert data["force"] is True
414
+ assert agent_status_mod.format_memory_cleanup(data).startswith("dry_run:")
415
+
416
+
276
417
  def test_agent_status_text_output(mp_with_cid):
277
418
  pkg = mp_with_cid(_FAKE_CID)
278
419
  _insert_task(pkg, "tk_status", "Build a compact menu-bar status app.", rss_mb=100)
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+
9
+ def _bootstrap(tmp_path, monkeypatch, *, interval="86400", disable_bg="1"):
10
+ env = {
11
+ "THREADKEEPER_DB": str(tmp_path / "db.sqlite"),
12
+ "CLAUDE_PROJECTS_DIR": str(tmp_path / "fake_claude_projects"),
13
+ "THREADKEEPER_AUTO_UPDATE_INTERVAL_S": interval,
14
+ "THREADKEEPER_AUTO_UPDATE_RESTART": "0",
15
+ "THREADKEEPER_DISABLE_BG_DAEMONS": disable_bg,
16
+ "THREADKEEPER_INGEST_INTERVAL_S": "0",
17
+ "THREADKEEPER_INGEST_CAP": "0",
18
+ "THREADKEEPER_SKILL_WATCH_INTERVAL_S": "0",
19
+ "THREADKEEPER_SPAWN_BUDGET_POLL_S": "0",
20
+ "THREADKEEPER_MEMORY_GUARD_POLL_S": "0",
21
+ "THREADKEEPER_SEARCH_PROXY_POLL_S": "0",
22
+ "THREADKEEPER_SHADOW_REVIEW_INTERVAL_S": "0",
23
+ "THREADKEEPER_CURATOR_INTERVAL_S": "0",
24
+ "THREADKEEPER_EXTRACT_INTERVAL_S": "0",
25
+ "THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S": "0",
26
+ "THREADKEEPER_PROBE_INTERVAL_S": "0",
27
+ "THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S": "0",
28
+ "THREADKEEPER_THREAD_JANITOR_INTERVAL_S": "0",
29
+ "THREADKEEPER_TASK_LOG_DIR": str(tmp_path / "tasks"),
30
+ "THREADKEEPER_CLIENT": "pytest",
31
+ "THREADKEEPER_NO_EMBEDDINGS": "1",
32
+ }
33
+ for key, value in env.items():
34
+ monkeypatch.setenv(key, value)
35
+ Path(env["CLAUDE_PROJECTS_DIR"]).mkdir(parents=True, exist_ok=True)
36
+ for name in [m for m in list(sys.modules) if m.startswith("threadkeeper")]:
37
+ del sys.modules[name]
38
+ import threadkeeper.server # noqa: F401
39
+ from threadkeeper import auto_update, db
40
+
41
+ return {"auto_update": auto_update, "db": db}
42
+
43
+
44
+ def test_disabled_without_force(tmp_path, monkeypatch):
45
+ pkg = _bootstrap(tmp_path, monkeypatch, interval="0")
46
+
47
+ assert pkg["auto_update"].run_auto_update_pass() == "disabled"
48
+
49
+
50
+ def test_force_pass_records_event(tmp_path, monkeypatch):
51
+ pkg = _bootstrap(tmp_path, monkeypatch, interval="86400")
52
+ monkeypatch.setattr(
53
+ pkg["auto_update"],
54
+ "_request_and_apply_update",
55
+ lambda: "no_update mode=test",
56
+ )
57
+
58
+ out = pkg["auto_update"].run_auto_update_pass(
59
+ force=True,
60
+ restart_on_update=False,
61
+ )
62
+
63
+ assert out == "no_update mode=test"
64
+ row = pkg["db"].get_db().execute(
65
+ "SELECT summary FROM events WHERE kind='auto_update_pass' "
66
+ "ORDER BY id DESC LIMIT 1"
67
+ ).fetchone()
68
+ assert row is not None
69
+ assert row["summary"] == "no_update mode=test"
70
+
71
+
72
+ def test_recent_pass_makes_daemon_tick_not_due(tmp_path, monkeypatch):
73
+ pkg = _bootstrap(tmp_path, monkeypatch, interval="86400")
74
+ conn = pkg["db"].get_db()
75
+ conn.execute(
76
+ "INSERT INTO events (session_id, kind, target, summary, created_at) "
77
+ "VALUES ('s', 'auto_update_pass', '', 'no_update', ?)",
78
+ (int(time.time()),),
79
+ )
80
+ conn.commit()
81
+ called = {"value": False}
82
+
83
+ def fake_update():
84
+ called["value"] = True
85
+ return "updated mode=test"
86
+
87
+ monkeypatch.setattr(pkg["auto_update"], "_request_and_apply_update", fake_update)
88
+
89
+ out = pkg["auto_update"].run_auto_update_pass(restart_on_update=False)
90
+
91
+ assert out.startswith("not_due age_s=")
92
+ assert called["value"] is False
93
+
94
+
95
+ def test_git_checkout_with_dirty_tracked_files_is_skipped(tmp_path, monkeypatch):
96
+ pkg = _bootstrap(tmp_path, monkeypatch)
97
+
98
+ def fake_git_stdout(repo, *args, timeout=60):
99
+ if args[:2] == ("status", "--porcelain"):
100
+ return 0, " M threadkeeper/server.py", ""
101
+ raise AssertionError(f"unexpected git call: {args}")
102
+
103
+ monkeypatch.setattr(pkg["auto_update"], "_git_stdout", fake_git_stdout)
104
+
105
+ assert (
106
+ pkg["auto_update"]._update_git_checkout(tmp_path)
107
+ == "skipped_dirty_checkout mode=git"
108
+ )
109
+
110
+
111
+ def test_pip_update_runs_setup_when_version_changes(tmp_path, monkeypatch):
112
+ pkg = _bootstrap(tmp_path, monkeypatch)
113
+ versions = iter(["0.9.2", "0.9.3"])
114
+ calls: list[list[str]] = []
115
+
116
+ def fake_run(args, *, cwd=None, timeout=None):
117
+ calls.append(args)
118
+ return SimpleNamespace(returncode=0, stdout="", stderr="")
119
+
120
+ monkeypatch.setattr(pkg["auto_update"], "_installed_version", lambda: next(versions))
121
+ monkeypatch.setattr(pkg["auto_update"], "_package_spec", lambda: "threadkeeper")
122
+ monkeypatch.setattr(pkg["auto_update"], "_run", fake_run)
123
+ monkeypatch.setattr(pkg["auto_update"], "_run_setup", lambda: " setup=ok")
124
+
125
+ out = pkg["auto_update"]._update_installed_package()
126
+
127
+ assert out == "updated mode=pip old=0.9.2 new=0.9.3 setup=ok"
128
+ assert calls == [
129
+ [sys.executable, "-m", "pip", "install", "--upgrade", "threadkeeper"]
130
+ ]
131
+
132
+
133
+ def test_daemon_does_not_start_when_background_daemons_disabled(tmp_path, monkeypatch):
134
+ pkg = _bootstrap(tmp_path, monkeypatch, interval="86400", disable_bg="1")
135
+
136
+ pkg["auto_update"].start_auto_update_daemon()
137
+
138
+ assert pkg["auto_update"]._started is False
@@ -31,6 +31,8 @@ def test_defaults_match(monkeypatch):
31
31
  assert c.SKILL_NUDGE_INTERVAL == 10
32
32
  assert c.BRIEF_LEAN is False
33
33
  assert c.SPAWN_BUDGET_MB == 3072
34
+ assert c.AUTO_UPDATE_INTERVAL_S == 86400
35
+ assert c.AUTO_UPDATE_RESTART is True
34
36
  assert str(c.DB_PATH).endswith("/.threadkeeper/db.sqlite")
35
37
 
36
38
 
@@ -70,6 +72,9 @@ def test_all_exported_names_present(monkeypatch):
70
72
  c = _fresh_config(monkeypatch)
71
73
  required = [
72
74
  "AUTO_REVIEW_ENABLED",
75
+ "AUTO_UPDATE_INTERVAL_S",
76
+ "AUTO_UPDATE_RESTART",
77
+ "AUTO_UPDATE_TIMEOUT_S",
73
78
  "BACKGROUND_DAEMONS_ALLOWED",
74
79
  "BRIEF_LEAN",
75
80
  "BRIEF_NO_THREAD_NUDGE",