memstack-skill-loader 4.18.0__tar.gz → 4.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {memstack_skill_loader-4.18.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.19.0}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/agent_runner.py +101 -20
  5. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/dashboard.html +561 -110
  6. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/dashboard.py +58 -9
  7. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/memory_db.py +97 -52
  8. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/memory_migrations.py +125 -47
  9. memstack_skill_loader-4.19.0/src/memstack_skill_loader/models.py +785 -0
  10. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/stats_tracker.py +53 -3
  11. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/stats.py +250 -48
  12. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  13. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +6 -0
  14. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_advisor_strategy.py +4 -0
  15. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_builder_scope.py +4 -0
  16. memstack_skill_loader-4.19.0/tests/test_burn_reconciliation.py +323 -0
  17. memstack_skill_loader-4.19.0/tests/test_credential_status.py +540 -0
  18. memstack_skill_loader-4.19.0/tests/test_dashboard_model_select.py +144 -0
  19. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_memory_migrations.py +192 -36
  20. memstack_skill_loader-4.19.0/tests/test_model_registry.py +411 -0
  21. memstack_skill_loader-4.19.0/tests/test_session_store.py +141 -0
  22. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/MANIFEST.in +0 -0
  23. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/README.md +0 -0
  24. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/setup.cfg +0 -0
  25. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/__main__.py +0 -0
  26. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/categories.py +0 -0
  27. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/compression.py +0 -0
  28. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/config.py +0 -0
  29. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/diary_ingest.py +0 -0
  30. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/goal.py +0 -0
  31. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/goal_engine.py +0 -0
  32. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/hooks/__init__.py +0 -0
  33. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/hooks/deny_git.py +0 -0
  34. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/hooks/deny_manager_write.py +0 -0
  35. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/hooks/deny_reviewer_git.py +0 -0
  36. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/hooks/git_policy.py +0 -0
  37. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/index_meta.py +0 -0
  38. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/indexer.py +0 -0
  39. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/license.py +0 -0
  40. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/memory_engine.py +0 -0
  41. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/__init__.py +0 -0
  42. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/_diag.py +0 -0
  43. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/body_parser.py +0 -0
  44. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/compressor.py +0 -0
  45. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/forwarder.py +0 -0
  46. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/pro_compressor.py +0 -0
  47. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/proxy/server.py +0 -0
  48. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/search.py +0 -0
  49. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/server.py +0 -0
  50. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/session_context.py +0 -0
  51. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/skill_config.py +0 -0
  52. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/skill_descriptions.json +0 -0
  53. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
  54. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader/version_check.py +0 -0
  55. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  56. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  57. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  58. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
  59. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_body_parser_metric.py +0 -0
  60. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_code_read_savings.py +0 -0
  61. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_dashscan.py +0 -0
  62. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_diary_ingest.py +0 -0
  63. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_failed_approaches.py +0 -0
  64. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_git_policy.py +0 -0
  65. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_index_meta_guard.py +0 -0
  66. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_license_environment.py +0 -0
  67. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_license_grace.py +0 -0
  68. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_memory_engine.py +0 -0
  69. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_memory_inspector.py +0 -0
  70. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_memory_scoring.py +0 -0
  71. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_memory_tools.py +0 -0
  72. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_pro_compressor.py +0 -0
  73. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_pro_skills_update.py +0 -0
  74. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_pro_source_resolution.py +0 -0
  75. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_project_dir_canonical.py +0 -0
  76. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_search_memory.py +0 -0
  77. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_session_context.py +0 -0
  78. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_skill_drift.py +0 -0
  79. {memstack_skill_loader-4.18.0 → memstack_skill_loader-4.19.0}/tests/test_wheel_contents.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.18.0
3
+ Version: 4.19.0
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Project-URL: Homepage, https://memstack.pro
6
6
  Project-URL: Documentation, https://memstack.pro/docs
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.18.0"
7
+ version = "4.19.0"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.18.0"
3
+ __version__ = "4.19.0"
@@ -20,19 +20,14 @@ from .stats import log_agent_invocation, log_api_cost
20
20
 
21
21
 
22
22
  # ---------------------------------------------------------------------------
23
- # Anthropic model pricing (per million tokens, May 2026)
23
+ # Anthropic model pricing
24
24
  # ---------------------------------------------------------------------------
25
+ # The rate card lives in models.py, which is also what the dashboard reads, so
26
+ # there is one table rather than two that can drift. models.MODEL_PRICING is
27
+ # prefix-matched, so a dated id such as claude-haiku-4-5-20251001 resolves to
28
+ # its family rate instead of falling through to the unknown-model default.
29
+ from .models import MODEL_PRICING, price_for as _price_for # noqa: F401
25
30
 
26
- MODEL_PRICING = {
27
- "claude-fable-5": {"input": 10.0, "output": 50.0},
28
- "claude-opus-5": {"input": 5.0, "output": 25.0},
29
- "claude-sonnet-5": {"input": 3.0, "output": 15.0},
30
- "claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
31
- "claude-opus-4-6": {"input": 5.0, "output": 25.0},
32
- "claude-opus-4-7": {"input": 5.0, "output": 25.0},
33
- "claude-opus-4-8": {"input": 5.0, "output": 25.0},
34
- "claude-haiku-4-5": {"input": 1.0, "output": 5.0},
35
- }
36
31
  # Cache read: 90% discount on input rate
37
32
  # Cache creation: 25% premium on input rate
38
33
 
@@ -44,8 +39,15 @@ def _calculate_api_cost(
44
39
  cache_read_tokens: int = 0,
45
40
  cache_creation_tokens: int = 0,
46
41
  ) -> float:
47
- """Calculate USD cost from token counts and model pricing."""
48
- pricing = MODEL_PRICING.get(model, MODEL_PRICING["claude-sonnet-4-6"])
42
+ """Calculate USD cost from token counts and model pricing.
43
+
44
+ Returns 0.0 for an id with no list rate (a non-model placeholder such as
45
+ "<synthetic>"): there is no rate to multiply by, and inventing one at the
46
+ Opus rate is what this avoids.
47
+ """
48
+ pricing = _price_for(model)
49
+ if not pricing["has_rate"]:
50
+ return 0.0
49
51
  input_rate = pricing["input"]
50
52
  output_rate = pricing["output"]
51
53
 
@@ -71,6 +73,9 @@ MAX_ITERATIONS = 9999
71
73
 
72
74
  ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "")
73
75
  API_KEY_FILE = Path.home() / ".memstack" / "api_key"
76
+ # Holds the Claude Code SUBSCRIPTION token (sk-ant-oat01...), NOT an API key.
77
+ # The file name says "oauth" for install-compatibility only; do not rename it.
78
+ # An api03 key filed here is sent as a Bearer token and silently never works.
74
79
  OAUTH_TOKEN_FILE = Path.home() / ".memstack" / "oauth_token"
75
80
  API_DEFAULT_MODEL = "claude-sonnet-4-6"
76
81
  API_MAX_TOKENS = 16000
@@ -94,7 +99,12 @@ def _load_api_key() -> str | None:
94
99
  return None
95
100
 
96
101
  def _load_oauth_token() -> str | None:
97
- """Get the OAuth token from environment or stored config file."""
102
+ """Get the Claude Code SUBSCRIPTION token (sk-ant-oat01...).
103
+
104
+ Reads CLAUDE_CODE_OAUTH_TOKEN, then ~/.memstack/oauth_token. That file holds
105
+ the subscription token from `claude setup-token`, not an API key, despite
106
+ the historic file name.
107
+ """
98
108
  token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip()
99
109
  if token:
100
110
  return token
@@ -108,6 +118,54 @@ def _load_oauth_token() -> str | None:
108
118
  return None
109
119
 
110
120
 
121
+ def _live_api_key():
122
+ """(has_live_api_key, dead_rows) for the api-execution-mode fallback gate.
123
+
124
+ Validity, not presence. A key counts only when a probe of GET /v1/models no
125
+ older than 24h says the credential is live; the probe runs here when the
126
+ status file is absent or stale. ANTHROPIC_BASE_URL is a proxy address, not a
127
+ credential, and never counts. The old gate let it stand in for one, so a
128
+ machine with the TokenStack proxy exported ran "api" mode with no key at all.
129
+
130
+ Fails open on any error: an unreachable probe returns no live key, which
131
+ downgrades to subscription rather than running api mode blind.
132
+ """
133
+ try:
134
+ from . import models as model_registry
135
+ return model_registry.live_api_key_status()
136
+ except Exception as exc:
137
+ return False, [{"label": "probe", "kind_label": "",
138
+ "status": "unchecked", "error": str(exc)}]
139
+
140
+
141
+ def _dead_key_reason(dead_rows) -> str:
142
+ """One sentence naming which key was dead. Never any part of a value."""
143
+ if not dead_rows:
144
+ return "no API key credential found (env ANTHROPIC_API_KEY or ~/.memstack/api_key)"
145
+ parts = []
146
+ for r in dead_rows:
147
+ label = r.get("label", "?")
148
+ kind = r.get("kind_label", "")
149
+ code = r.get("http_status")
150
+ if r.get("status") == "dead":
151
+ detail = f"dead (HTTP {code})" if code else "dead"
152
+ else:
153
+ detail = "unchecked" + (f": {r.get('error', '')}" if r.get("error") else "")
154
+ parts.append(f"{label}" + (f" [{kind}]" if kind else "") + f": {detail}")
155
+ return "; ".join(parts)
156
+
157
+
158
+ def _append_agent_log(path: Path, line: str) -> None:
159
+ """Append one timestamped runner line to an agent log. Never raises."""
160
+ try:
161
+ path.parent.mkdir(parents=True, exist_ok=True)
162
+ ts = time.strftime("%Y-%m-%dT%H:%M:%S")
163
+ with open(path, "a", encoding="utf-8") as f:
164
+ f.write(f"[{ts}] [agent-runner] {line}\n")
165
+ except Exception:
166
+ pass
167
+
168
+
111
169
  SYSTEM_PROMPTS = {
112
170
  "manager": (
113
171
  "You are a Manager agent. You receive a task and project context (directory listing "
@@ -1071,6 +1129,8 @@ def _build_env(strip_api_key: bool = False, inject_api_key: str | None = None) -
1071
1129
  # Set strip_api_key=False to revert.
1072
1130
  if strip_api_key:
1073
1131
  env.pop("ANTHROPIC_API_KEY", None)
1132
+ # Subscription token (sk-ant-oat01) from ~/.memstack/oauth_token: that
1133
+ # file holds the Claude Code subscription token, not an API key.
1074
1134
  oauth_token = _load_oauth_token()
1075
1135
  if oauth_token:
1076
1136
  env["ANTHROPIC_AUTH_TOKEN"] = oauth_token
@@ -1094,11 +1154,17 @@ def _build_env(strip_api_key: bool = False, inject_api_key: str | None = None) -
1094
1154
  return env
1095
1155
 
1096
1156
 
1097
- def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int, str]:
1157
+ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int, str, int, int]:
1098
1158
  """Extract text and token usage from claude --output-format stream-json.
1099
1159
 
1100
- Returns (text, input_tokens, output_tokens, cost_usd, context_tokens, model).
1160
+ Returns (text, input_tokens, output_tokens, cost_usd, context_tokens, model,
1161
+ cache_read_tokens, cache_creation_tokens).
1101
1162
  context_tokens is the last iteration's total tokens (accurate context window fill).
1163
+
1164
+ The two cache populations are returned separately because the CLI bills for
1165
+ them inside total_cost_usd while leaving them out of usage.input_tokens.
1166
+ Dropping them here is what made the Burn Report's cost columns unprovable
1167
+ from its token columns.
1102
1168
  """
1103
1169
  text_parts: list[str] = []
1104
1170
  tool_parts: list[str] = []
@@ -1108,6 +1174,8 @@ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int, str]:
1108
1174
  cost_usd = 0.0
1109
1175
  context_tokens = 0
1110
1176
  stream_model = ""
1177
+ cache_read_tokens = 0
1178
+ cache_creation_tokens = 0
1111
1179
  last_assistant_usage: dict = {}
1112
1180
  for line in raw.splitlines():
1113
1181
  line = line.strip()
@@ -1139,6 +1207,8 @@ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int, str]:
1139
1207
  usage = obj.get("usage", {})
1140
1208
  input_tokens = usage.get("input_tokens", 0)
1141
1209
  output_tokens = usage.get("output_tokens", 0)
1210
+ cache_read_tokens = usage.get("cache_read_input_tokens", 0) or 0
1211
+ cache_creation_tokens = usage.get("cache_creation_input_tokens", 0) or 0
1142
1212
  cost_usd = obj.get("total_cost_usd", 0.0) or 0.0
1143
1213
  if last_assistant_usage:
1144
1214
  context_tokens = (
@@ -1156,7 +1226,8 @@ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int, str]:
1156
1226
  text = "[Agent completed with tool calls but no text summary]\n\n" + "\n\n".join(tool_parts)
1157
1227
  else:
1158
1228
  text = ""
1159
- return text, input_tokens, output_tokens, cost_usd, context_tokens, stream_model
1229
+ return (text, input_tokens, output_tokens, cost_usd, context_tokens,
1230
+ stream_model, cache_read_tokens, cache_creation_tokens)
1160
1231
 
1161
1232
 
1162
1233
  def _extract_text_from_stream_line(line: str) -> Optional[str]:
@@ -1574,7 +1645,8 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
1574
1645
 
1575
1646
  raw_stdout = stdout_data or ""
1576
1647
  stderr = stderr_data or ""
1577
- output, input_tokens, output_tokens, cost_usd, context_tokens, stream_model = _parse_stream_json(raw_stdout)
1648
+ (output, input_tokens, output_tokens, cost_usd, context_tokens, stream_model,
1649
+ cache_read_tokens, cache_creation_tokens) = _parse_stream_json(raw_stdout)
1578
1650
  output = output.strip()
1579
1651
  ts = time.strftime("%H:%M:%S")
1580
1652
 
@@ -1615,6 +1687,8 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
1615
1687
  input_tokens=input_tokens, output_tokens=output_tokens,
1616
1688
  actual_cost_usd=cost_usd, execution_mode=exec_mode,
1617
1689
  project=_project,
1690
+ cache_read_tokens=cache_read_tokens,
1691
+ cache_creation_tokens=cache_creation_tokens,
1618
1692
  )
1619
1693
  except Exception:
1620
1694
  pass
@@ -2091,11 +2165,18 @@ def _orchestrate(session: Session) -> None:
2091
2165
  # Load execution modes and custom display names for this run
2092
2166
  agent_modes = load_agent_modes()
2093
2167
  display_names = _load_agent_display_names()
2094
- has_api_key = bool(_load_api_key() or ANTHROPIC_BASE_URL)
2168
+ has_api_key, dead_keys = _live_api_key()
2095
2169
  for _ag in ("manager", "builder", "reviewer"):
2096
2170
  if agent_modes[_ag] == "api" and not has_api_key:
2097
2171
  agent_modes[_ag] = "subscription"
2098
- print(f"[agent-runner] {_ag}: mode=api but no API key found, falling back to subscription", file=sys.stderr)
2172
+ reason = _dead_key_reason(dead_keys)
2173
+ print(f"[agent-runner] {_ag}: mode=api but no live API key, "
2174
+ f"falling back to subscription ({reason})", file=sys.stderr)
2175
+ _append_agent_log(
2176
+ session_log_dir / f"{_ag}.log",
2177
+ f"mode=api requested but no live API key; "
2178
+ f"falling back to subscription. {reason}",
2179
+ )
2099
2180
 
2100
2181
  # Step 1: Manager analyzes the task
2101
2182
  session.agents["manager"]["status"] = "busy"