Capability-set targeting + Claude Code headless provider + resilience fixes
Capability targeting (new feature): - loop --capability <SET|ALL> scopes which capability library the loop reasons against. data/capability_sets/<name>.json (same schema as capabilities.json) holds one-off targeted products; drop a file in and pass its name, no code changes. First set: agentminder (Broadcom AgentMinder, agentic-AI identity). - capabilities.set_active_set() swaps the active library; keyword pre-filter is bypassed for small targeted sets so every signal is evaluated. New 'capability-sets' command lists them; run banner shows the active set. LLM provider — the '429' was a disguised policy block: - Anthropic rejects Claude Code OAuth tokens on /v1/messages for non-Claude-Code traffic with a fake rate_limit_error (no anthropic-ratelimit-* headers). New 'claude-cli' provider runs inference through the sanctioned headless path (claude -p --append-system-prompt --model), now the default provider. - OAuth keychain token auto-refresh via headless claude when expired. Resilience: - STATE_DIR is overridable (macOS endpoint-security locked output/state via com.apple.macl); load_memory tolerates PermissionError; exa usage counter tolerates malformed files; run.py handles Ctrl-C cleanly (no traceback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ python3 run.py loop --no-domain-filter # Exa: broad-web news (default)
|
||||
| `--tier T` | Only `strategic` / `enterprise` / `growth` accounts |
|
||||
| `--rep NAME` | Only accounts assigned to a rep (case-insensitive name or `rep_id` substring) |
|
||||
| `--limit N` | Process at most N accounts. Order: **strategic tier first, then least-recently-scanned** (never-scanned first). The run cursor (`output/state/run_cursor.json`) persists, so successive `--limit` runs walk the whole territory — e.g. a nightly `loop --limit 30` covers 342 accounts in ~12 days. |
|
||||
| `--capability SET` | Capability set to reason against: `ALL` (full 40-capability portfolio, default) or a named set from `data/capability_sets/` (e.g. `agentminder`). In a targeted set the keyword pre-filter is bypassed so EVERY signal is evaluated against the chosen product. List sets: `python3 run.py capability-sets`. **Add a new one-off product**: drop `data/capability_sets/<name>.json` (same schema as `capabilities.json`) and pass `--capability <name>` — no code changes. |
|
||||
| `--domain-filter` | Scope Exa news to the account's own domain (newsroom/PR only) |
|
||||
| `--no-domain-filter` | Broad-web news about the account (default) |
|
||||
|
||||
|
||||
50
sales-agent/data/capability_sets/agentminder.json
Normal file
50
sales-agent/data/capability_sets/agentminder.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"lastUpdated": "2026-07-16",
|
||||
"_source": "https://www.broadcom.com/products/identity/agentminder",
|
||||
"_note": "One-off targeted capability set. Select at runtime: `loop --capability agentminder`. Add more sets as data/capability_sets/<name>.json with this same schema.",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "cap_iam_agentminder",
|
||||
"grouping": "Cybersecurity — Identity & Access Management",
|
||||
"category": "Agentic AI Security",
|
||||
"name": "AgentMinder — Secure Agentic AI (Identity & Governance for AI Agents)",
|
||||
"description": "Agentic security and governance platform that turns autonomous AI agents into production-grade 'digital employees': governs AI agents as first-class identities with least-privilege access, credential protection, centralized authentication/authorization, and lifecycle governance, on a cloud-native container-based architecture.",
|
||||
"businessProblems": [
|
||||
"AI agents/copilots being deployed with no identity, credential, or access governance",
|
||||
"Non-human identities (service accounts, bots, AI agents) proliferating outside IAM controls",
|
||||
"No least-privilege or audit trail for what autonomous agents can access and do",
|
||||
"Compliance/regulatory exposure from ungoverned agentic AI acting on production systems",
|
||||
"Security teams blocking AI initiatives because agent access cannot be controlled"
|
||||
],
|
||||
"buyingSignals": [
|
||||
"Announcements of agentic AI, AI agents, or copilot deployments in production",
|
||||
"AI transformation or GenAI initiatives touching core business systems",
|
||||
"Hiring for AI security, AI governance, ML platform, or non-human identity roles",
|
||||
"Executive posts about AI agents as 'digital workers' or 'digital employees'",
|
||||
"AI governance, responsible-AI, or AI-risk programs and committees being stood up",
|
||||
"Security incidents, breaches, or audit findings involving automation or bots",
|
||||
"Regulatory or compliance pressure on AI usage (finance, healthcare, government)",
|
||||
"Partnerships with AI platform vendors (OpenAI, Anthropic, Microsoft Copilot, agent frameworks)"
|
||||
],
|
||||
"antiSignals": [
|
||||
"Already owns AgentMinder per CRM",
|
||||
"Standardized on a competing agentic-AI security platform",
|
||||
"Explicit company policy prohibiting AI agent deployment"
|
||||
],
|
||||
"targetBuyer": {
|
||||
"titles": ["CISO", "Chief AI Officer", "VP Security", "Director IAM", "VP AI/ML Platform", "CTO"],
|
||||
"departments": ["Security", "IT", "AI/Data", "Risk & Compliance"]
|
||||
},
|
||||
"keywords": [
|
||||
"agentminder", "agentic ai", "ai agent", "ai agents", "autonomous agent",
|
||||
"digital employee", "digital worker", "copilot", "non-human identity", "nhi",
|
||||
"machine identity", "ai governance", "ai security", "least privilege",
|
||||
"agent identity", "genai", "generative ai", "ai risk", "responsible ai",
|
||||
"service account", "credential", "zero trust"
|
||||
]
|
||||
}
|
||||
],
|
||||
"bundleSignals": [],
|
||||
"globalAntiSignals": []
|
||||
}
|
||||
@@ -22,6 +22,8 @@ from src.memory import store, feedback
|
||||
|
||||
|
||||
def cmd_loop(args):
|
||||
from src import capabilities
|
||||
capabilities.set_active_set(args.capability)
|
||||
if args.no_domain_filter:
|
||||
config.EXA_DOMAIN_FILTER = False # per-run override
|
||||
if args.domain_filter:
|
||||
@@ -95,6 +97,19 @@ def cmd_recalibrate(args):
|
||||
print(f" {k}: weight={v['weight']} (n={v['samples']})")
|
||||
|
||||
|
||||
def cmd_capability_sets(args):
|
||||
from src import capabilities
|
||||
import json as _json
|
||||
for name in capabilities.available_sets():
|
||||
if name == "ALL":
|
||||
n = len(_json.loads((config.DATA_DIR / "capabilities.json").read_text())["capabilities"])
|
||||
print(f" ALL {n} capabilities (full portfolio, default)")
|
||||
else:
|
||||
doc = _json.loads((config.DATA_DIR / "capability_sets" / f"{name}.json").read_text())
|
||||
caps = ", ".join(c["name"].split(" — ")[0] for c in doc["capabilities"])
|
||||
print(f" {name:12} {len(doc['capabilities'])} capability(ies): {caps}")
|
||||
|
||||
|
||||
def cmd_accounts(args):
|
||||
for a in crm.load_accounts():
|
||||
prods = a.get("currentProducts") or []
|
||||
@@ -137,6 +152,9 @@ def main(argv=None):
|
||||
p_loop.add_argument("--limit", type=int,
|
||||
help="process at most N accounts (strategic + least-recently-run first); "
|
||||
"successive runs resume where the last left off")
|
||||
p_loop.add_argument("--capability", default="ALL", metavar="SET",
|
||||
help="capability set to reason against: ALL (full portfolio, default) "
|
||||
"or a name from data/capability_sets/ (e.g. agentminder)")
|
||||
p_loop.add_argument("--no-domain-filter", action="store_true",
|
||||
help="broad-web news about the account (default behavior)")
|
||||
p_loop.add_argument("--domain-filter", action="store_true",
|
||||
@@ -168,13 +186,20 @@ def main(argv=None):
|
||||
p_re = sub.add_parser("recalibrate", help="Recompute feedback weights (weekly job)")
|
||||
p_re.set_defaults(func=cmd_recalibrate)
|
||||
|
||||
sub.add_parser("capability-sets", help="List available capability sets"
|
||||
).set_defaults(func=cmd_capability_sets)
|
||||
sub.add_parser("accounts", help="List target accounts").set_defaults(func=cmd_accounts)
|
||||
sub.add_parser("briefs", help="List all generated briefs").set_defaults(func=cmd_briefs)
|
||||
sub.add_parser("exa-usage", help="Show Exa.ai request budget").set_defaults(func=cmd_exa_usage)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
config.ensure_dirs()
|
||||
args.func(args)
|
||||
try:
|
||||
args.func(args)
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted — no partial briefs were written; unprocessed "
|
||||
"signals will be retried on the next run.")
|
||||
return 130
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
The design uses a vector ANN pre-filter to cut LLM calls ~60%. For the MVP we
|
||||
approximate that with keyword overlap scoring (no embedding service required).
|
||||
The interface is the same: given a signal, return candidate capabilities.
|
||||
|
||||
CAPABILITY SETS: the loop can target a subset of the portfolio at runtime.
|
||||
- "ALL" (default): data/capabilities.json — the full portfolio.
|
||||
- "<name>": data/capability_sets/<name>.json — a one-off targeted set
|
||||
(same schema). Add new products by dropping another file in that folder and
|
||||
passing `loop --capability <name>`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,10 +18,43 @@ from functools import lru_cache
|
||||
|
||||
from . import config
|
||||
|
||||
# Selected via set_active_set() before the loop runs; "ALL" = full portfolio.
|
||||
_active_set = "ALL"
|
||||
|
||||
|
||||
def available_sets() -> list[str]:
|
||||
sets_dir = config.DATA_DIR / "capability_sets"
|
||||
names = sorted(p.stem for p in sets_dir.glob("*.json")) if sets_dir.exists() else []
|
||||
return ["ALL"] + names
|
||||
|
||||
|
||||
def set_active_set(name: str) -> None:
|
||||
"""Select which capability library the loop reasons against."""
|
||||
global _active_set
|
||||
name = (name or "ALL").strip()
|
||||
if name.upper() == "ALL":
|
||||
_active_set = "ALL"
|
||||
else:
|
||||
path = config.DATA_DIR / "capability_sets" / f"{name}.json"
|
||||
if not path.exists():
|
||||
raise SystemExit(
|
||||
f"Unknown capability set '{name}'. Available: {', '.join(available_sets())}"
|
||||
)
|
||||
_active_set = name
|
||||
load_library.cache_clear()
|
||||
|
||||
|
||||
def active_set() -> str:
|
||||
return _active_set
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_library() -> dict:
|
||||
return json.loads((config.DATA_DIR / "capabilities.json").read_text())
|
||||
if _active_set == "ALL":
|
||||
return json.loads((config.DATA_DIR / "capabilities.json").read_text())
|
||||
return json.loads(
|
||||
(config.DATA_DIR / "capability_sets" / f"{_active_set}.json").read_text()
|
||||
)
|
||||
|
||||
|
||||
def capability_by_id(cap_id: str) -> dict | None:
|
||||
@@ -33,7 +72,13 @@ def prefilter(signal_content: str, top_k: int = 3) -> list[dict]:
|
||||
"""Return capabilities whose keywords overlap the signal, best first.
|
||||
|
||||
Stand-in for the vector ANN pre-filter in the design (section 5.2).
|
||||
In a targeted set (<=5 capabilities) the pre-filter is skipped: the whole
|
||||
point of a targeted run is to evaluate EVERY signal against the chosen
|
||||
product, and the LLM prompt stays small anyway.
|
||||
"""
|
||||
caps = load_library()["capabilities"]
|
||||
if _active_set != "ALL" and len(caps) <= 5:
|
||||
return caps
|
||||
sig_tokens = _tokens(signal_content)
|
||||
scored: list[tuple[float, dict]] = []
|
||||
for cap in load_library()["capabilities"]:
|
||||
|
||||
@@ -6,9 +6,6 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = ROOT / "data"
|
||||
OUTPUT_DIR = ROOT / "output"
|
||||
STATE_DIR = OUTPUT_DIR / "state"
|
||||
BRIEFS_DIR = OUTPUT_DIR / "briefs"
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
@@ -27,6 +24,15 @@ def _load_dotenv() -> None:
|
||||
|
||||
_load_dotenv()
|
||||
|
||||
# Paths (after dotenv so OUTPUT_DIR/STATE_DIR overrides in .env apply).
|
||||
# STATE_DIR is overridable because macOS TCC/endpoint-security software can
|
||||
# permanently lock a directory tree (com.apple.macl xattr) — pointing at a
|
||||
# fresh dir is the recovery path (state is regenerable). Relative values are
|
||||
# resolved against the sales-agent root.
|
||||
OUTPUT_DIR = ROOT / os.environ.get("OUTPUT_DIR", "output")
|
||||
STATE_DIR = (ROOT / os.environ.get("STATE_DIR", "")) if os.environ.get("STATE_DIR") else OUTPUT_DIR / "state"
|
||||
BRIEFS_DIR = OUTPUT_DIR / "briefs"
|
||||
|
||||
# --- LLM ---
|
||||
# Provider abstraction: openai | gemini | anthropic. If LLM_PROVIDER is unset we
|
||||
# auto-detect from whichever key is present (OpenAI-compatible first). There is NO
|
||||
@@ -66,6 +72,16 @@ LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-5")
|
||||
# keychain ("Claude Code-credentials"), read at runtime so refreshes are picked up.
|
||||
ANTHROPIC_OAUTH_TOKEN = os.environ.get("ANTHROPIC_OAUTH_TOKEN", "").strip()
|
||||
ANTHROPIC_USE_KEYCHAIN = os.environ.get("ANTHROPIC_USE_KEYCHAIN", "1").strip() == "1"
|
||||
# When the keychain token is expired, run a headless `claude` command to make
|
||||
# Claude Code refresh it, then re-read — the loop self-heals with no human step.
|
||||
ANTHROPIC_AUTOREFRESH = os.environ.get("ANTHROPIC_AUTOREFRESH", "1").strip() == "1"
|
||||
|
||||
# "claude-cli" provider: run inference through Claude Code's official headless
|
||||
# mode (`claude -p`). This is the sanctioned way to use a Claude subscription
|
||||
# programmatically — direct API calls with the Claude Code OAuth token are
|
||||
# policy-blocked (they return a fake 429 "Error"). Requires `claude` on PATH.
|
||||
CLAUDE_CLI_MODEL = os.environ.get("CLAUDE_CLI_MODEL", "sonnet").strip()
|
||||
CLAUDE_CLI_TIMEOUT = int(os.environ.get("CLAUDE_CLI_TIMEOUT", "300"))
|
||||
# Effort level for reasoning models (Sonnet 5 etc.): low | medium | high | max.
|
||||
ANTHROPIC_EFFORT = os.environ.get("ANTHROPIC_EFFORT", "low").strip()
|
||||
|
||||
@@ -74,14 +90,14 @@ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
|
||||
|
||||
_provider = os.environ.get("LLM_PROVIDER", "").strip().lower()
|
||||
if not _provider:
|
||||
if ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY:
|
||||
if ANTHROPIC_API_KEY:
|
||||
_provider = "anthropic"
|
||||
elif OPENAI_API_KEY:
|
||||
_provider = "openai"
|
||||
elif GEMINI_API_KEY:
|
||||
_provider = "gemini"
|
||||
else:
|
||||
_provider = "none"
|
||||
_provider = "claude-cli" # subscription via Claude Code headless mode
|
||||
# Selecting a provider whose key is missing => no usable provider.
|
||||
# ("anthropic" is exempt here: the token may come from the keychain at runtime.)
|
||||
if _provider == "openai" and not OPENAI_API_KEY:
|
||||
@@ -100,6 +116,8 @@ LLM_CONFIGURED = LLM_PROVIDER != "none"
|
||||
def llm_label() -> str:
|
||||
if LLM_PROVIDER == "none":
|
||||
return "NONE (no LLM provider configured)"
|
||||
if LLM_PROVIDER == "claude-cli":
|
||||
return f"Claude Code headless ({CLAUDE_CLI_MODEL}, subscription)"
|
||||
if LLM_PROVIDER == "openai":
|
||||
host = OPENAI_BASE_URL.split("//")[-1].split("/")[0]
|
||||
return f"OpenAI-compatible ({OPENAI_MODEL} @ {host})"
|
||||
|
||||
@@ -39,11 +39,14 @@ def _current_month() -> str:
|
||||
def _load_usage() -> dict:
|
||||
config.ensure_dirs()
|
||||
p = _usage_path()
|
||||
if p.exists():
|
||||
data = json.loads(p.read_text())
|
||||
if data.get("month") == _current_month():
|
||||
return data
|
||||
# New file or a new month -> reset the counter.
|
||||
try:
|
||||
if p.exists():
|
||||
data = json.loads(p.read_text())
|
||||
if data.get("month") == _current_month() and isinstance(data.get("count"), int):
|
||||
return data
|
||||
except (OSError, ValueError):
|
||||
pass # unreadable/corrupt file -> reset below
|
||||
# New file, new month, or malformed -> reset the counter.
|
||||
return {"month": _current_month(), "count": 0}
|
||||
|
||||
|
||||
|
||||
@@ -164,9 +164,36 @@ def _call_gemini(system: str, user: str, max_tokens: int) -> dict:
|
||||
_keychain_token_cache: dict = {}
|
||||
|
||||
|
||||
def _read_keychain_cred() -> dict:
|
||||
import subprocess
|
||||
raw = subprocess.run(
|
||||
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
).stdout.strip()
|
||||
return json.loads(raw).get("claudeAiOauth", {})
|
||||
|
||||
|
||||
def _refresh_via_claude_cli() -> bool:
|
||||
"""Make Claude Code refresh its own OAuth token by running a tiny headless
|
||||
command. Returns True if the CLI ran (the keychain should then be fresh)."""
|
||||
import subprocess
|
||||
try:
|
||||
print(" [llm] keychain OAuth token expired — auto-refreshing via `claude -p` ...")
|
||||
subprocess.run(
|
||||
["claude", "-p", "reply: ok", "--model", "haiku"],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" [llm] auto-refresh failed: {str(e)[:80]}")
|
||||
return False
|
||||
|
||||
|
||||
def _anthropic_oauth_token() -> str:
|
||||
"""Resolve an OAuth access token: env var first, then the Claude Code
|
||||
credential stored in the macOS keychain (kept fresh by Claude Code)."""
|
||||
credential in the macOS keychain. If the keychain token is expired and
|
||||
ANTHROPIC_AUTOREFRESH=1 (default), a headless `claude` call refreshes it
|
||||
automatically — the loop self-heals without human intervention."""
|
||||
if config.ANTHROPIC_OAUTH_TOKEN:
|
||||
return config.ANTHROPIC_OAUTH_TOKEN
|
||||
if not config.ANTHROPIC_USE_KEYCHAIN:
|
||||
@@ -176,20 +203,19 @@ def _anthropic_oauth_token() -> str:
|
||||
if _keychain_token_cache.get("expires", 0) > now:
|
||||
return _keychain_token_cache["token"]
|
||||
try:
|
||||
import subprocess
|
||||
raw = subprocess.run(
|
||||
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
).stdout.strip()
|
||||
cred = json.loads(raw).get("claudeAiOauth", {})
|
||||
cred = _read_keychain_cred()
|
||||
expired = cred.get("accessToken") and cred.get("expiresAt", 0) / 1000 < now
|
||||
if expired and config.ANTHROPIC_AUTOREFRESH and _refresh_via_claude_cli():
|
||||
cred = _read_keychain_cred() # re-read after refresh
|
||||
expired = cred.get("accessToken") and cred.get("expiresAt", 0) / 1000 < time.time()
|
||||
token = cred.get("accessToken", "")
|
||||
exp_ms = cred.get("expiresAt", 0)
|
||||
if token and exp_ms and exp_ms / 1000 < now:
|
||||
if token and expired:
|
||||
raise LLMNotConfigured(
|
||||
"The Claude Code OAuth token in the keychain is EXPIRED "
|
||||
f"(expired {time.strftime('%Y-%m-%d %H:%M', time.localtime(exp_ms/1000))}). "
|
||||
"Run any command in the Claude Code CLI (e.g. `claude -p hi`) to refresh it, "
|
||||
"or set ANTHROPIC_OAUTH_TOKEN / ANTHROPIC_API_KEY in .env."
|
||||
f"(expired {time.strftime('%Y-%m-%d %H:%M', time.localtime(cred.get('expiresAt', 0)/1000))}) "
|
||||
"and auto-refresh did not produce a fresh one. Run `claude -p hi` manually, "
|
||||
"or set ANTHROPIC_OAUTH_TOKEN (long-lived token from `claude setup-token`) "
|
||||
"or ANTHROPIC_API_KEY in .env."
|
||||
)
|
||||
if token:
|
||||
_keychain_token_cache.update({"token": token, "expires": now + 60})
|
||||
@@ -230,7 +256,38 @@ def _call_anthropic(system: str, user: str, max_tokens: int) -> dict:
|
||||
return _extract_json(text)
|
||||
|
||||
|
||||
def _call_claude_cli(system: str, user: str, max_tokens: int) -> dict:
|
||||
"""Inference through Claude Code's official headless mode (`claude -p`).
|
||||
|
||||
This is the sanctioned way to run subscription inference programmatically:
|
||||
direct /v1/messages calls with a Claude Code OAuth token are policy-blocked
|
||||
(the API returns a disguised 429 'Error' for non-Claude-Code traffic).
|
||||
"""
|
||||
import subprocess
|
||||
cmd = [
|
||||
"claude", "-p", user,
|
||||
"--append-system-prompt", system + "\nReturn ONLY a JSON object. No prose, no markdown fences.",
|
||||
"--model", config.CLAUDE_CLI_MODEL,
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True,
|
||||
timeout=config.CLAUDE_CLI_TIMEOUT)
|
||||
except FileNotFoundError:
|
||||
raise LLMNotConfigured("`claude` CLI not found on PATH (needed for LLM_PROVIDER=claude-cli).")
|
||||
except subprocess.TimeoutExpired:
|
||||
raise LLMRetryable(f"claude CLI timed out after {config.CLAUDE_CLI_TIMEOUT}s")
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "").strip()[:300]
|
||||
# Usage-limit messages from the CLI are worth waiting out like a 429.
|
||||
if "limit" in err.lower():
|
||||
raise LLMRetryable(f"claude CLI usage limit: {err}")
|
||||
raise LLMError(f"claude CLI failed (rc={proc.returncode}): {err}")
|
||||
return _extract_json(proc.stdout.strip())
|
||||
|
||||
|
||||
def _dispatch(system: str, user: str, max_tokens: int) -> dict:
|
||||
if config.LLM_PROVIDER == "claude-cli":
|
||||
return _call_claude_cli(system, user, max_tokens)
|
||||
if config.LLM_PROVIDER == "openai":
|
||||
return _call_openai(system, user, max_tokens)
|
||||
if config.LLM_PROVIDER == "gemini":
|
||||
|
||||
@@ -19,8 +19,13 @@ def _account_path(account_id: str) -> Path:
|
||||
def load_memory(account_id: str) -> dict:
|
||||
config.ensure_dirs()
|
||||
p = _account_path(account_id)
|
||||
if p.exists():
|
||||
return json.loads(p.read_text())
|
||||
try:
|
||||
if p.exists():
|
||||
return json.loads(p.read_text())
|
||||
except (PermissionError, OSError) as e:
|
||||
# macOS TCC/endpoint security can lock individual files (com.apple.macl);
|
||||
# start fresh rather than killing the whole run — memory is regenerable.
|
||||
print(f" [memory] cannot read {p.name} ({e.__class__.__name__}) — starting fresh")
|
||||
return {
|
||||
"accountId": account_id,
|
||||
"signalHashes": [], # [{hash, discoveredAt, signalType}]
|
||||
|
||||
@@ -161,8 +161,12 @@ def run_all(
|
||||
total = len(accounts)
|
||||
if limit and limit > 0:
|
||||
accounts = accounts[:limit]
|
||||
from . import capabilities
|
||||
cap_set = capabilities.active_set()
|
||||
n_caps = len(capabilities.load_library()["capabilities"])
|
||||
print(f"Running prospecting loop over {len(accounts)}/{total} account(s) "
|
||||
f"(strategic + least-recently-run first) — LLM: {config.llm_label()}")
|
||||
print(f"Capability set: {cap_set} ({n_caps} capabilit{'y' if n_caps == 1 else 'ies'})")
|
||||
if config.EXA_ENABLED:
|
||||
st = exa.usage_status()
|
||||
scope = "domain-scoped" if config.EXA_DOMAIN_FILTER else "broad web"
|
||||
|
||||
Reference in New Issue
Block a user