agentpool-cli 0.1.7__tar.gz → 0.1.8__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 (121) hide show
  1. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/CHANGELOG.md +8 -0
  2. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/PKG-INFO +7 -3
  3. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/README.md +6 -2
  4. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/agentpool-skill.md +3 -0
  5. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/usage-detection.md +9 -2
  6. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/pyproject.toml +1 -1
  7. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/server.json +2 -2
  8. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/__init__.py +1 -1
  9. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/config.py +8 -0
  10. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/mcp/tools.py +1 -1
  11. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/onboarding.py +2 -1
  12. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/session_manager.py +34 -1
  13. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/compute.py +2 -3
  14. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/summary.py +1 -4
  15. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_models_config_store.py +79 -0
  16. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_usage_summary_enrichment.py +5 -4
  17. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/uv.lock +1 -1
  18. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.cursor/mcp.json.example +0 -0
  19. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/CODEOWNERS +0 -0
  20. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/ISSUE_TEMPLATE/provider_probe.md +0 -0
  22. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/dependabot.yml +0 -0
  23. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/workflows/ci.yml +0 -0
  24. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.github/workflows/release.yml +0 -0
  25. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.gitignore +0 -0
  26. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/.mcp.json.example +0 -0
  27. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/AGENTS.md +0 -0
  28. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/CONTRIBUTING.md +0 -0
  29. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/LICENSE +0 -0
  30. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/SECURITY.md +0 -0
  31. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/agent-cli-and-mcp.md +0 -0
  32. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/architecture.md +0 -0
  33. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/examples/README.md +0 -0
  34. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/examples.md +0 -0
  35. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/install.md +0 -0
  36. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/mcp-clients.md +0 -0
  37. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/mcp-tools.md +0 -0
  38. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/model-catalog.md +0 -0
  39. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/onboarding.md +0 -0
  40. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/provider-adapters.md +0 -0
  41. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/provider-lifecycle-matrix.md +0 -0
  42. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/quickstart.md +0 -0
  43. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/release.md +0 -0
  44. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/security.md +0 -0
  45. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-claude-code.md +0 -0
  46. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-codex.md +0 -0
  47. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-copilot.md +0 -0
  48. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-cursor-cli.md +0 -0
  49. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-cursor.md +0 -0
  50. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-devin.md +0 -0
  51. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/setup-droid.md +0 -0
  52. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/stats.md +0 -0
  53. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/docs/usage-probe-matrix.md +0 -0
  54. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/scripts/install.sh +0 -0
  55. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/agent_io.py +0 -0
  56. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/artifacts.py +0 -0
  57. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/cli.py +0 -0
  58. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/event_detection.py +0 -0
  59. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/__init__.py +0 -0
  60. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/__init__.py +0 -0
  61. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_approval_agent.py +0 -0
  62. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_common.py +0 -0
  63. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_completed_agent.py +0 -0
  64. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_idle_agent.py +0 -0
  65. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_limit_agent.py +0 -0
  66. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_patch_agent.py +0 -0
  67. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/fixtures/fake_agents/fake_question_agent.py +0 -0
  68. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/git_worktree.py +0 -0
  69. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/mcp/__init__.py +0 -0
  70. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/mcp/resources.py +0 -0
  71. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/mcp_server.py +0 -0
  72. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/models.py +0 -0
  73. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/policy.py +0 -0
  74. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/preferences.py +0 -0
  75. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/provider_model_catalog.json +0 -0
  76. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/providers/__init__.py +0 -0
  77. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/providers/base.py +0 -0
  78. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/providers/registry.py +0 -0
  79. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/redaction.py +0 -0
  80. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/runtimes/__init__.py +0 -0
  81. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/runtimes/base.py +0 -0
  82. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/runtimes/tmux.py +0 -0
  83. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/__init__.py +0 -0
  84. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/card.py +0 -0
  85. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/queries.py +0 -0
  86. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/render.py +0 -0
  87. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/stats/window.py +0 -0
  88. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/store.py +0 -0
  89. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/__init__.py +0 -0
  90. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/_common.py +0 -0
  91. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/ccusage.py +0 -0
  92. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/claude.py +0 -0
  93. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/codex.py +0 -0
  94. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/codexbar.py +0 -0
  95. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/combine.py +0 -0
  96. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/copilot.py +0 -0
  97. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/devin.py +0 -0
  98. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/parsers.py +0 -0
  99. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/probes.py +0 -0
  100. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/usage/provider_parsers.py +0 -0
  101. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/src/agentpool/utils.py +0 -0
  102. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/provider_model_catalog_golden.json +0 -0
  103. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/stats_seed.py +0 -0
  104. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/usage/claude_usage.txt +0 -0
  105. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/usage/codex_rate_limits.json +0 -0
  106. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/usage/copilot_user.json +0 -0
  107. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/fixtures/usage/devin_plan_status.json +0 -0
  108. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/integration/test_fake_tmux_flow.py +0 -0
  109. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_agent_io.py +0 -0
  110. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_cli.py +0 -0
  111. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_event_policy.py +0 -0
  112. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_mcp_surface.py +0 -0
  113. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_mcp_tools.py +0 -0
  114. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_onboarding.py +0 -0
  115. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_redaction.py +0 -0
  116. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_stats_cli.py +0 -0
  117. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_stats_mcp.py +0 -0
  118. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_stats_window.py +0 -0
  119. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_subprocess_safety.py +0 -0
  120. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_usage_probes.py +0 -0
  121. {agentpool_cli-0.1.7 → agentpool_cli-0.1.8}/tests/unit/test_usage_provider_parsers.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.8 - 2026-06-01
6
+
7
+ - Treat stale usage snapshots as age metadata, not as an unusable-provider
8
+ reason. `usage-summary` still reports `stale` and `age_seconds`, but a stale
9
+ cache entry no longer overrides usable quota/status data.
10
+ - Add optional `policy.usage_auto_refresh_after_seconds` for users who want
11
+ cached usage summaries to refresh automatically after a configured age.
12
+
5
13
  ## 0.1.7 - 2026-06-01
6
14
 
7
15
  - Centralize non-interactive subprocess execution behind terminal-safe helpers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentpool-cli
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom.
5
5
  Author: AgentPool contributors
6
6
  License-Expression: MIT
@@ -171,14 +171,18 @@ agentpool usage-summary --refresh --backend ccusage --provider claude-code --jso
171
171
 
172
172
  `usage-summary` returns a `providers` object keyed by provider id. It is not
173
173
  ordered and it is not a recommendation list. Each row includes `usable`,
174
- `unusable_reason`, quota windows, confidence, staleness, and reset timing when
174
+ `unusable_reason`, quota windows, confidence, age/staleness, and reset timing when
175
175
  the provider exposes it. The older CLI `capacity-summary` command is retained
176
176
  as a human convenience alias; the MCP surface only exposes `get_usage_snapshot`
177
177
  and the opt-in `get_usage_summary` tool.
178
178
 
179
179
  The default buffer is `policy.min_remaining_percent = 10`. If any reported
180
180
  quota window is below that buffer, the provider row is marked unusable for the
181
- summary. AgentPool still does not pick an alternative provider for you.
181
+ summary. Staleness is reported as age information only; it does not by itself
182
+ make a provider unusable. If you want cached summary reads to refresh
183
+ automatically after a threshold, set `policy.usage_auto_refresh_after_seconds`
184
+ in `~/.agentpool/config.yaml`.
185
+ AgentPool still does not pick an alternative provider for you.
182
186
  MCP usage refreshes are intentionally bounded and may return `partial=true`;
183
187
  use the CLI commands above when a shell-capable agent needs a complete live
184
188
  refresh.
@@ -150,14 +150,18 @@ agentpool usage-summary --refresh --backend ccusage --provider claude-code --jso
150
150
 
151
151
  `usage-summary` returns a `providers` object keyed by provider id. It is not
152
152
  ordered and it is not a recommendation list. Each row includes `usable`,
153
- `unusable_reason`, quota windows, confidence, staleness, and reset timing when
153
+ `unusable_reason`, quota windows, confidence, age/staleness, and reset timing when
154
154
  the provider exposes it. The older CLI `capacity-summary` command is retained
155
155
  as a human convenience alias; the MCP surface only exposes `get_usage_snapshot`
156
156
  and the opt-in `get_usage_summary` tool.
157
157
 
158
158
  The default buffer is `policy.min_remaining_percent = 10`. If any reported
159
159
  quota window is below that buffer, the provider row is marked unusable for the
160
- summary. AgentPool still does not pick an alternative provider for you.
160
+ summary. Staleness is reported as age information only; it does not by itself
161
+ make a provider unusable. If you want cached summary reads to refresh
162
+ automatically after a threshold, set `policy.usage_auto_refresh_after_seconds`
163
+ in `~/.agentpool/config.yaml`.
164
+ AgentPool still does not pick an alternative provider for you.
161
165
  MCP usage refreshes are intentionally bounded and may return `partial=true`;
162
166
  use the CLI commands above when a shell-capable agent needs a complete live
163
167
  refresh.
@@ -27,6 +27,9 @@ need to delegate coding-agent work.
27
27
  live refresh. MCP `refresh=true` is bounded and can return `partial=true`
28
28
  with unknown rows for slow providers.
29
29
  - Treat usage rows as a provider-id map. They are not ordered and not ranked.
30
+ - Treat `stale` and `age_seconds` as age metadata, not as an instruction to
31
+ avoid a provider. If the user configured usage auto-refresh, cached summary
32
+ reads may refresh themselves before returning.
30
33
  - Inspect provider models before spawning when the model is not already chosen:
31
34
  - CLI: `agentpool models --provider <provider-id>`
32
35
  - MCP: `get_provider_models(provider_id=...)`
@@ -9,11 +9,18 @@ Live probes are only run by explicit usage requests. Inventory remains non-invas
9
9
  `agentpool usage-summary` returns a `providers` map keyed by provider id. The
10
10
  CLI `capacity-summary` command is a human convenience alias; MCP does not expose
11
11
  a capacity alias. Each row includes `usable`, `unusable_reason`, `stale`, and
12
- `age_seconds`. `usable` is conservative: unknown, unauthenticated, unavailable,
13
- stale, untrusted-confidence, or below-buffer windows are unusable. The default
12
+ `age_seconds`. `stale` is informational age metadata only; it does not by
13
+ itself make a provider unusable. `usable` is derived from install/auth status,
14
+ provider usage status, confidence, and reported quota windows. The default
14
15
  buffer is `policy.min_remaining_percent = 10`, and it applies to every reported
15
16
  quota window.
16
17
 
18
+ Cached summary reads can optionally refresh themselves when old enough. Set
19
+ `policy.usage_auto_refresh_after_seconds` to a non-negative number in
20
+ `~/.agentpool/config.yaml` to refresh `usage-summary` / `get_usage_summary`
21
+ when the cached summary data is missing or older than that threshold. Leave it
22
+ as `null` to keep refreshes explicit.
23
+
17
24
  Usage windows carry a stable `kind` in addition to provider-specific names:
18
25
 
19
26
  - `daily`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentpool-cli"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "Make full use of every coding-agent subscription you pay for: a local CLI + MCP server that surfaces live usage limits and offloads work to providers with headroom."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.sidduHERE/agentpool",
4
4
  "title": "AgentPool",
5
5
  "description": "See each coding-agent subscription's live limits and offload work to one with headroom.",
6
- "version": "0.1.7",
6
+ "version": "0.1.8",
7
7
  "repository": {
8
8
  "url": "https://github.com/sidduHERE/agentpool",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "pypi",
15
15
  "identifier": "agentpool-cli",
16
- "version": "0.1.7",
16
+ "version": "0.1.8",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },
@@ -1,3 +1,3 @@
1
1
  """AgentPool local agent control plane."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.8"
@@ -68,6 +68,7 @@ class PolicyConfig(BaseModel):
68
68
  default_isolation: str = "read_only"
69
69
  min_remaining_percent: int = 10
70
70
  usage_stale_after_seconds: int = 1800
71
+ usage_auto_refresh_after_seconds: int | None = None
71
72
  allowed_providers: list[str] = Field(default_factory=list)
72
73
  denied_providers: list[str] = Field(default_factory=list)
73
74
  block_on_usage_statuses: list[str] = Field(
@@ -326,6 +327,13 @@ def validate_config(config: AgentPoolConfig) -> dict[str, Any]:
326
327
  errors.append("policy.allow_auto_routing must stay false in AgentPool v0.1")
327
328
  if "auto" in config.providers:
328
329
  errors.append("providers.auto is not allowed")
330
+ if config.policy.usage_stale_after_seconds < 0:
331
+ errors.append("policy.usage_stale_after_seconds must be non-negative")
332
+ if (
333
+ config.policy.usage_auto_refresh_after_seconds is not None
334
+ and config.policy.usage_auto_refresh_after_seconds < 0
335
+ ):
336
+ errors.append("policy.usage_auto_refresh_after_seconds must be non-negative or null")
329
337
  if "factory-droid" in config.providers:
330
338
  warnings.append("factory-droid is a PRD compatibility name; use droid-cli for the droid binary")
331
339
  for provider_id, reason in DEPRECATED_PROVIDER_IDS.items():
@@ -68,7 +68,7 @@ def get_usage_summary(
68
68
  refresh=refresh,
69
69
  backend=backend,
70
70
  allow_interactive=False,
71
- timeout_seconds=timeout_seconds if refresh else None,
71
+ timeout_seconds=timeout_seconds,
72
72
  )
73
73
 
74
74
 
@@ -492,7 +492,8 @@ def privacy_doctor(manager: SessionManager) -> dict[str, Any]:
492
492
  },
493
493
  "usage_refresh": {
494
494
  "inventory_runs_live_usage_probes": False,
495
- "live_usage_requires_explicit_refresh": True,
495
+ "live_usage_requires_explicit_refresh": manager.config.policy.usage_auto_refresh_after_seconds is None,
496
+ "summary_auto_refresh_after_seconds": manager.config.policy.usage_auto_refresh_after_seconds,
496
497
  "default_backend": "combined",
497
498
  "optional_backends": {
498
499
  "codexbar": detect_codexbar(),
@@ -155,9 +155,22 @@ class SessionManager:
155
155
  for snapshot in snapshots:
156
156
  self.store.save_usage_snapshot(snapshot)
157
157
  source = "live_probe"
158
+ summary_backend = backend
158
159
  else:
159
160
  snapshots = self._configured_usage_snapshots(self.store.latest_usage_snapshots(provider_id), provider_id)
160
161
  source = "sqlite_cache"
162
+ summary_backend = "cache"
163
+ if self._usage_summary_should_auto_refresh(snapshots, provider_id):
164
+ snapshots = self.registry.usage(
165
+ provider_id,
166
+ backend=backend,
167
+ allow_interactive=allow_interactive,
168
+ timeout_seconds=_remaining_seconds(deadline),
169
+ )
170
+ for snapshot in snapshots:
171
+ self.store.save_usage_snapshot(snapshot)
172
+ source = "live_probe"
173
+ summary_backend = backend
161
174
  return {
162
175
  **build_usage_summary(
163
176
  snapshots,
@@ -167,7 +180,7 @@ class SessionManager:
167
180
  ),
168
181
  "preferences": preferences_reference(),
169
182
  "source": source,
170
- "backend": backend if refresh else "cache",
183
+ "backend": summary_backend,
171
184
  "partial": _has_partial_usage(snapshots) or _has_partial_descriptors(descriptors),
172
185
  }
173
186
 
@@ -865,6 +878,26 @@ class SessionManager:
865
878
  configured = set(self.config.providers)
866
879
  return [snapshot for snapshot in snapshots if snapshot.provider_id in configured]
867
880
 
881
+ def _usage_summary_should_auto_refresh(self, snapshots: list[Any], provider_id: str | None = None) -> bool:
882
+ threshold = self.config.policy.usage_auto_refresh_after_seconds
883
+ if threshold is None:
884
+ return False
885
+ if provider_id and not snapshots:
886
+ return True
887
+ if not provider_id:
888
+ configured = set(self.config.providers)
889
+ seen = {snapshot.provider_id for snapshot in snapshots}
890
+ if not configured.issubset(seen):
891
+ return True
892
+ now = datetime.now(timezone.utc)
893
+ for snapshot in snapshots:
894
+ checked_at = snapshot.checked_at
895
+ if checked_at.tzinfo is None:
896
+ checked_at = checked_at.replace(tzinfo=timezone.utc)
897
+ if max(0.0, (now - checked_at).total_seconds()) > threshold:
898
+ return True
899
+ return False
900
+
868
901
  def _enforce_deadline(self, session: AgentSession) -> ObserveWorkerResponse | None:
869
902
  deadline_at = session.metadata.get("deadline_at")
870
903
  if not deadline_at or not active_state(session.state):
@@ -457,9 +457,8 @@ def _snapshot_usability(
457
457
  }
458
458
  for window in snapshot.windows
459
459
  ]
460
- age_seconds = max(0.0, (at - _ensure_utc(snapshot.checked_at)).total_seconds())
461
- stale = age_seconds > stale_after_seconds
462
- return _usable_reason(snapshot, windows, min_remaining_percent, descriptor, stale)
460
+ _ = stale_after_seconds
461
+ return _usable_reason(snapshot, windows, min_remaining_percent, descriptor)
463
462
 
464
463
 
465
464
  def _is_quota_unusable_reason(reason: str | None) -> bool:
@@ -65,7 +65,7 @@ def _provider_summary(
65
65
  ]
66
66
  age_seconds = max(0.0, (now - snapshot.checked_at).total_seconds())
67
67
  stale = age_seconds > stale_after_seconds
68
- usable, unusable_reason = _usable_reason(snapshot, windows, min_remaining_percent, descriptor, stale)
68
+ usable, unusable_reason = _usable_reason(snapshot, windows, min_remaining_percent, descriptor)
69
69
  return {
70
70
  "provider_id": snapshot.provider_id,
71
71
  "status": snapshot.status.value if hasattr(snapshot.status, "value") else str(snapshot.status),
@@ -90,7 +90,6 @@ def _usable_reason(
90
90
  windows: list[dict[str, Any]],
91
91
  min_remaining_percent: int,
92
92
  descriptor: ProviderDescriptor | None,
93
- stale: bool,
94
93
  ) -> tuple[bool, str | None]:
95
94
  if descriptor and not descriptor.installed:
96
95
  return False, "not_installed"
@@ -110,8 +109,6 @@ def _usable_reason(
110
109
  }
111
110
  if confidence not in allowed_confidence:
112
111
  return False, f"confidence_{confidence}"
113
- if stale:
114
- return False, "usage_stale"
115
112
  for window in windows:
116
113
  remaining = window["remaining_percent"]
117
114
  if remaining is not None and remaining < min_remaining_percent:
@@ -842,6 +842,85 @@ def test_cached_usage_summary_filters_removed_provider_aliases(tmp_path: Path) -
842
842
  assert "factory-droid" not in provider_ids
843
843
 
844
844
 
845
+ def test_usage_summary_auto_refresh_is_opt_in_and_bounded(tmp_path: Path) -> None:
846
+ class RefreshingRegistry:
847
+ def __init__(self) -> None:
848
+ self.calls: list[dict[str, object]] = []
849
+
850
+ def descriptors(self, include_usage: bool = False, timeout_seconds: float | None = None): # type: ignore[no-untyped-def]
851
+ return []
852
+
853
+ def usage(
854
+ self,
855
+ provider_id: str | None = None,
856
+ *,
857
+ backend: str = "combined",
858
+ allow_interactive: bool = True,
859
+ timeout_seconds: float | None = None,
860
+ ): # type: ignore[no-untyped-def]
861
+ self.calls.append(
862
+ {
863
+ "provider_id": provider_id,
864
+ "backend": backend,
865
+ "allow_interactive": allow_interactive,
866
+ "timeout_seconds": timeout_seconds,
867
+ }
868
+ )
869
+ return [
870
+ CapacitySnapshot(
871
+ provider_id="codex-cli",
872
+ status=UsageStatus.AVAILABLE,
873
+ confidence=Confidence.OFFICIAL,
874
+ windows=[
875
+ UsageWindow(
876
+ name="5h",
877
+ kind=UsageWindowKind.FIVE_HOUR,
878
+ remaining_percent=90,
879
+ confidence=Confidence.OFFICIAL,
880
+ )
881
+ ],
882
+ )
883
+ ]
884
+
885
+ config = load_config(Path("__missing_agentpool_config__.yaml"))
886
+ config.storage = StorageConfig(db_path=str(tmp_path / "agentpool.sqlite"), artifact_root=str(tmp_path / "artifacts"))
887
+ config.policy.usage_auto_refresh_after_seconds = 1800
888
+ store = Store(tmp_path / "agentpool.sqlite")
889
+ store.save_usage_snapshot(
890
+ CapacitySnapshot(
891
+ provider_id="codex-cli",
892
+ status=UsageStatus.AVAILABLE,
893
+ confidence=Confidence.OFFICIAL,
894
+ checked_at=now_utc() - timedelta(hours=2),
895
+ windows=[
896
+ UsageWindow(
897
+ name="5h",
898
+ kind=UsageWindowKind.FIVE_HOUR,
899
+ remaining_percent=80,
900
+ confidence=Confidence.OFFICIAL,
901
+ )
902
+ ],
903
+ )
904
+ )
905
+ registry = RefreshingRegistry()
906
+ manager = SessionManager(config=config, store=store, registry=registry, runtime=RecordingRuntime()) # type: ignore[arg-type]
907
+
908
+ summary = manager.usage_summary(provider_id="codex-cli", refresh=False, backend="native", timeout_seconds=7)
909
+
910
+ assert summary["source"] == "live_probe"
911
+ assert summary["backend"] == "native"
912
+ assert summary["providers"]["codex-cli"]["usable"] is True
913
+ assert summary["providers"]["codex-cli"]["stale"] is False
914
+ assert registry.calls == [
915
+ {
916
+ "provider_id": "codex-cli",
917
+ "backend": "native",
918
+ "allow_interactive": True,
919
+ "timeout_seconds": pytest.approx(7, abs=0.1),
920
+ }
921
+ ]
922
+
923
+
845
924
  def test_failed_worktree_spawn_rolls_back_worktree_and_branch(tmp_path: Path) -> None:
846
925
  repo = tmp_path / "repo"
847
926
  repo.mkdir()
@@ -120,7 +120,7 @@ def test_usage_summary_auth_install_and_unknown_confidence_block_usable() -> Non
120
120
  assert summary["counts"]["usable"] == 0
121
121
 
122
122
 
123
- def test_usage_summary_marks_stale_snapshots_unusable() -> None:
123
+ def test_usage_summary_reports_stale_without_blocking_usable() -> None:
124
124
  snapshot = CapacitySnapshot(
125
125
  provider_id="codex-cli",
126
126
  status=UsageStatus.AVAILABLE,
@@ -138,9 +138,10 @@ def test_usage_summary_marks_stale_snapshots_unusable() -> None:
138
138
 
139
139
  summary = build_usage_summary([snapshot], stale_after_seconds=1800, provider_descriptors=[_descriptor("codex-cli")])
140
140
 
141
- assert summary["providers"]["codex-cli"]["usable"] is False
142
- assert summary["providers"]["codex-cli"]["unusable_reason"] == "usage_stale"
143
- assert summary["providers"]["codex-cli"]["stale"] is True
141
+ row = summary["providers"]["codex-cli"]
142
+ assert row["usable"] is True
143
+ assert row["unusable_reason"] is None
144
+ assert row["stale"] is True
144
145
 
145
146
 
146
147
  def _descriptor(provider_id: str, installed: bool = True, auth_status: str = "authenticated") -> ProviderDescriptor:
@@ -3,7 +3,7 @@ requires-python = ">=3.11"
3
3
 
4
4
  [[package]]
5
5
  name = "agentpool-cli"
6
- version = "0.1.7"
6
+ version = "0.1.8"
7
7
  source = { editable = "." }
8
8
  dependencies = [
9
9
  { name = "certifi" },
File without changes
File without changes
File without changes
File without changes