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.
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/PKG-INFO +54 -11
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/README.md +53 -10
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/pyproject.toml +10 -1
- {threadkeeper-0.9.1 → 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.1 → 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.1 → threadkeeper-0.10.0}/tests/test_thread_janitor.py +1 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/agent_status.py +127 -0
- threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/Info.plist +25 -0
- threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/README.md +83 -0
- threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +760 -0
- threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/build.sh +27 -0
- threadkeeper-0.10.0/threadkeeper/assets/macos-agent-status/install.sh +63 -0
- threadkeeper-0.10.0/threadkeeper/auto_update.py +347 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/config.py +8 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/identity.py +5 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/menubar_app.py +101 -5
- threadkeeper-0.10.0/threadkeeper/tools/agent_status.py +41 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dashboard.py +1 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/PKG-INFO +54 -11
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/SOURCES.txt +8 -0
- threadkeeper-0.9.1/threadkeeper/tools/agent_status.py +0 -19
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/LICENSE +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/setup.cfg +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_adapters.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_curator.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_identity.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_lessons.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_nudges.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_panel.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_process_health.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_skills.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_threads.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/gemini.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/db.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/server.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.9.1 → threadkeeper-0.10.0}/threadkeeper.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
220
|
-
probe, so a healthy cooldown shows `0 due
|
|
221
|
-
macOS, `python -m threadkeeper.server`
|
|
222
|
-
on MCP startup
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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.
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
179
|
-
probe, so a healthy cooldown shows `0 due
|
|
180
|
-
macOS, `python -m threadkeeper.server`
|
|
181
|
-
on MCP startup
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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",
|