threadkeeper 0.9.2__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.
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/PKG-INFO +47 -6
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/README.md +46 -5
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/pyproject.toml +1 -1
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_agent_status.py +141 -0
- threadkeeper-0.10.0/tests/test_auto_update.py +138 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_config_settings.py +5 -0
- threadkeeper-0.10.0/tests/test_menubar_app.py +186 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_thread_janitor.py +1 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/agent_status.py +127 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/assets/macos-agent-status/README.md +20 -2
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +306 -28
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/assets/macos-agent-status/install.sh +5 -0
- threadkeeper-0.10.0/threadkeeper/auto_update.py +347 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/config.py +8 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/identity.py +5 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/menubar_app.py +73 -3
- threadkeeper-0.10.0/threadkeeper/tools/agent_status.py +41 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/dashboard.py +1 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper.egg-info/PKG-INFO +47 -6
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper.egg-info/SOURCES.txt +2 -0
- threadkeeper-0.9.2/tests/test_menubar_app.py +0 -69
- threadkeeper-0.9.2/threadkeeper/tools/agent_status.py +0 -19
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/LICENSE +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/setup.cfg +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_adapters.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_curator.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_identity.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_lessons.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_nudges.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_panel.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_process_health.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_skills.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_threads.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/gemini.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/db.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/server.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.9.2 → threadkeeper-0.10.0}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.9.2 → 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.
|
|
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,6 +212,7 @@ 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
|
|
@@ -215,15 +222,45 @@ spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
|
215
222
|
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
216
223
|
does not need a git checkout for the widget to build. Active loops are sorted
|
|
217
224
|
first (`running`, then `ready`), so background work stays at the top of the
|
|
218
|
-
panel.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
235
|
+
autonomous child task produces a useful result in `recent_results`; the first
|
|
236
|
+
poll only marks existing results as seen, so old completions do not spam
|
|
237
|
+
notifications. Probe backlog is due objective
|
|
222
238
|
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
223
239
|
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
224
|
-
automatically installs and launches it on MCP startup
|
|
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
|
|
225
243
|
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
226
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
|
+
|
|
227
264
|
Manual fallback from a source checkout:
|
|
228
265
|
|
|
229
266
|
```sh
|
|
@@ -512,6 +549,9 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
|
|
|
512
549
|
|---|---|---|
|
|
513
550
|
| `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
|
|
514
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 |
|
|
515
555
|
| `THREADKEEPER_SHADOW_REVIEW_INTERVAL_S` | 0 (off) | shadow daemon tick (s) |
|
|
516
556
|
| `THREADKEEPER_SHADOW_REVIEW_WINDOW_S` | 900 | sliding window for shadow scan (s) |
|
|
517
557
|
| `THREADKEEPER_EXTRACT_INTERVAL_S` | 0 (off) | extract daemon tick (s); 600 = 10 min recommended |
|
|
@@ -525,6 +565,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
|
|
|
525
565
|
| `THREADKEEPER_PROBE_COOLDOWN_S` | 604800 | per-category probe cooldown; 86400 = 1d recommended for active reliability tracking |
|
|
526
566
|
| `THREADKEEPER_SPAWN_BUDGET_MB` | 3072 | combined child RSS cap (MB); 0 disables |
|
|
527
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 |
|
|
528
569
|
| `THREADKEEPER_MEMORY_GUARD_POLL_S` | 30 | server RSS guard tick (s); 0 disables |
|
|
529
570
|
| `THREADKEEPER_MEMORY_GUARD_WARN_MB` | 1536 | notify/log when a server crosses this RSS |
|
|
530
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,6 +171,7 @@ 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
|
|
@@ -174,15 +181,45 @@ spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
|
174
181
|
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
175
182
|
does not need a git checkout for the widget to build. Active loops are sorted
|
|
176
183
|
first (`running`, then `ready`), so background work stays at the top of the
|
|
177
|
-
panel.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
194
|
+
autonomous child task produces a useful result in `recent_results`; the first
|
|
195
|
+
poll only marks existing results as seen, so old completions do not spam
|
|
196
|
+
notifications. Probe backlog is due objective
|
|
181
197
|
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
182
198
|
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
183
|
-
automatically installs and launches it on MCP startup
|
|
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
|
|
184
202
|
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
185
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
|
+
|
|
186
223
|
Manual fallback from a source checkout:
|
|
187
224
|
|
|
188
225
|
```sh
|
|
@@ -471,6 +508,9 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
|
|
|
471
508
|
|---|---|---|
|
|
472
509
|
| `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
|
|
473
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 |
|
|
474
514
|
| `THREADKEEPER_SHADOW_REVIEW_INTERVAL_S` | 0 (off) | shadow daemon tick (s) |
|
|
475
515
|
| `THREADKEEPER_SHADOW_REVIEW_WINDOW_S` | 900 | sliding window for shadow scan (s) |
|
|
476
516
|
| `THREADKEEPER_EXTRACT_INTERVAL_S` | 0 (off) | extract daemon tick (s); 600 = 10 min recommended |
|
|
@@ -484,6 +524,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
|
|
|
484
524
|
| `THREADKEEPER_PROBE_COOLDOWN_S` | 604800 | per-category probe cooldown; 86400 = 1d recommended for active reliability tracking |
|
|
485
525
|
| `THREADKEEPER_SPAWN_BUDGET_MB` | 3072 | combined child RSS cap (MB); 0 disables |
|
|
486
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 |
|
|
487
528
|
| `THREADKEEPER_MEMORY_GUARD_POLL_S` | 30 | server RSS guard tick (s); 0 disables |
|
|
488
529
|
| `THREADKEEPER_MEMORY_GUARD_WARN_MB` | 1536 | notify/log when a server crosses this RSS |
|
|
489
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.
|
|
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" }]
|
|
@@ -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",
|