@agentikos/omega-os 0.1.0 → 0.19.5
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.
- package/README.md +56 -14
- package/bootstrap/lib/__pycache__/claude-code-settings.cpython-313.pyc +0 -0
- package/bootstrap/lib/__pycache__/llm-clis.cpython-313.pyc +0 -0
- package/bootstrap/lib/__pycache__/manifest-helpers.cpython-313.pyc +0 -0
- package/bootstrap/lib/claude-code-settings.py +176 -0
- package/bootstrap/lib/common.sh +457 -1
- package/bootstrap/lib/llm-clis.py +341 -0
- package/bootstrap/lib/manifest-helpers.py +384 -0
- package/bootstrap/lib/steps.sh +1000 -26
- package/bootstrap/manifest.example.yaml +93 -2
- package/bootstrap/templates/aisb/CLAUDE.md +305 -0
- package/bootstrap/templates/aisb/architect.md +204 -0
- package/bootstrap/templates/aisb/checkers/CLAUDE.md +9 -0
- package/bootstrap/templates/aisb/checkers/checker-architect.md +151 -0
- package/bootstrap/templates/aisb/checkers/checker-common.md +171 -0
- package/bootstrap/templates/aisb/checkers/checker-construct.md +129 -0
- package/bootstrap/templates/aisb/checkers/checker-keymaker.md +204 -0
- package/bootstrap/templates/aisb/checkers/checker-link.md +205 -0
- package/bootstrap/templates/aisb/checkers/checker-merovingian.md +219 -0
- package/bootstrap/templates/aisb/checkers/checker-morpheus.md +211 -0
- package/bootstrap/templates/aisb/checkers/checker-neo.md +177 -0
- package/bootstrap/templates/aisb/checkers/checker-niobe.md +156 -0
- package/bootstrap/templates/aisb/checkers/checker-oracle.md +164 -0
- package/bootstrap/templates/aisb/checkers/checker-seraph.md +187 -0
- package/bootstrap/templates/aisb/checkers/checker-smith.md +195 -0
- package/bootstrap/templates/aisb/checkers/checker-zion.md +113 -0
- package/bootstrap/templates/aisb/construct.md +135 -0
- package/bootstrap/templates/aisb/keymaker.md +227 -0
- package/bootstrap/templates/aisb/link.md +170 -0
- package/bootstrap/templates/aisb/lmc-protocol.md +57 -0
- package/bootstrap/templates/aisb/merovingian.md +159 -0
- package/bootstrap/templates/aisb/morpheus.md +243 -0
- package/bootstrap/templates/aisb/neo.md +147 -0
- package/bootstrap/templates/aisb/niobe.md +197 -0
- package/bootstrap/templates/aisb/oracle.md +244 -0
- package/bootstrap/templates/aisb/protocols/handoff-templates.md +204 -0
- package/bootstrap/templates/aisb/protocols/shared-protocol.md +248 -0
- package/bootstrap/templates/aisb/pythia.md +153 -0
- package/bootstrap/templates/aisb/seraph.md +315 -0
- package/bootstrap/templates/aisb/smith.md +202 -0
- package/bootstrap/templates/aisb/zion.md +172 -0
- package/bootstrap/templates/autonomous/audit-patrol.yaml +41 -0
- package/bootstrap/templates/autonomous/smith-reflect.yaml +43 -0
- package/bootstrap/templates/autonomous/ssh-key-rotate.yaml +46 -0
- package/bootstrap/templates/autonomous/support-agent.yaml +38 -0
- package/docs/AUDITS.md +85 -0
- package/docs/COMPLETION-PLAN.md +48 -0
- package/docs/GAP-ANALYSIS.md +214 -0
- package/docs/INSTALL.md +47 -9
- package/docs/MCP-AND-PLUGINS.md +31 -4
- package/docs/SIMULATION.md +171 -0
- package/docs/simulate.sh +211 -0
- package/install.sh +164 -17
- package/omega/Agentik_Engine/README.md +27 -10
- package/omega/Agentik_Engine/omega_engine/__init__.py +212 -2
- package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/account.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/agent_messages.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/aisb_chat.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_diff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_gate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/auto_update.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/backup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cadence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/classifier.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cleanup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/completions.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/costs.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/done_signal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/envelope.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/handoff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_bootstrap.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_desktop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/learning.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/managed_agent.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/memory.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/menu.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/plan.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/prompts.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/prune.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/pursue.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/skill_routing.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/smoke.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/sync.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/telegram_history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tools.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/understand_anything.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/updater.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/validate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/vault.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/webhooks.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/worker.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/account.py +502 -0
- package/omega/Agentik_Engine/omega_engine/agent_messages.py +167 -0
- package/omega/Agentik_Engine/omega_engine/aisb_chat.py +128 -0
- package/omega/Agentik_Engine/omega_engine/audit_diff.py +99 -0
- package/omega/Agentik_Engine/omega_engine/audit_gate.py +149 -0
- package/omega/Agentik_Engine/omega_engine/audits/__init__.py +60 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/batcher.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/dispatcher.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/generator.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/batcher.py +218 -0
- package/omega/Agentik_Engine/omega_engine/audits/dispatcher.py +92 -0
- package/omega/Agentik_Engine/omega_engine/audits/generator.py +234 -0
- package/omega/Agentik_Engine/omega_engine/audits/history.py +168 -0
- package/omega/Agentik_Engine/omega_engine/audits/pipeline.py +198 -0
- package/omega/Agentik_Engine/omega_engine/auto_update.py +339 -0
- package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
- package/omega/Agentik_Engine/omega_engine/backup.py +215 -0
- package/omega/Agentik_Engine/omega_engine/cadence.py +158 -0
- package/omega/Agentik_Engine/omega_engine/classifier.py +215 -0
- package/omega/Agentik_Engine/omega_engine/cleanup.py +673 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +4564 -56
- package/omega/Agentik_Engine/omega_engine/completions.py +260 -0
- package/omega/Agentik_Engine/omega_engine/costs.py +100 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/engine.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/telegram.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
- package/omega/Agentik_Engine/omega_engine/daemons/engine.py +236 -0
- package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +315 -0
- package/omega/Agentik_Engine/omega_engine/done_signal.py +154 -0
- package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/artifact.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/automation.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/base.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/claudecode.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/connection.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/coworker.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/loop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/prompt.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/skill.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
- package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
- package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
- package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
- package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
- package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
- package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
- package/omega/Agentik_Engine/omega_engine/envelope.py +219 -0
- package/omega/Agentik_Engine/omega_engine/executor.py +195 -16
- package/omega/Agentik_Engine/omega_engine/genesis/__init__.py +134 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/phases.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/stack.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/state.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/orchestrator.py +262 -0
- package/omega/Agentik_Engine/omega_engine/genesis/phases.py +950 -0
- package/omega/Agentik_Engine/omega_engine/genesis/stack.py +324 -0
- package/omega/Agentik_Engine/omega_engine/genesis/state.py +353 -0
- package/omega/Agentik_Engine/omega_engine/handoff.py +459 -0
- package/omega/Agentik_Engine/omega_engine/hermes.py +426 -0
- package/omega/Agentik_Engine/omega_engine/hermes_bootstrap.py +382 -0
- package/omega/Agentik_Engine/omega_engine/hermes_desktop.py +469 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__init__.py +30 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/graphify.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/integrations/graphify.py +234 -0
- package/omega/Agentik_Engine/omega_engine/learning.py +268 -0
- package/omega/Agentik_Engine/omega_engine/managed_agent.py +467 -0
- package/omega/Agentik_Engine/omega_engine/memory.py +271 -0
- package/omega/Agentik_Engine/omega_engine/menu.py +1065 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__init__.py +144 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/v0_14_0.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/migrations/v0_14_0.py +29 -0
- package/omega/Agentik_Engine/omega_engine/mission.py +29 -14
- package/omega/Agentik_Engine/omega_engine/plan.py +846 -0
- package/omega/Agentik_Engine/omega_engine/prompts.py +158 -0
- package/omega/Agentik_Engine/omega_engine/provider.py +408 -13
- package/omega/Agentik_Engine/omega_engine/prune.py +151 -0
- package/omega/Agentik_Engine/omega_engine/pursue.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/agentic.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/base.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/corrective.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/graph.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/hybrid.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/multimodal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
- package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
- package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
- package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
- package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
- package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
- package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
- package/omega/Agentik_Engine/omega_engine/router.py +28 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__init__.py +48 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/auditor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/finder.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/installer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/marketplaces.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/auditor.py +232 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/finder.py +94 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/installer.py +129 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/marketplaces.py +80 -0
- package/omega/Agentik_Engine/omega_engine/skill_routing.py +388 -0
- package/omega/Agentik_Engine/omega_engine/smoke.py +81 -0
- package/omega/Agentik_Engine/omega_engine/store.py +132 -25
- package/omega/Agentik_Engine/omega_engine/sync.py +445 -0
- package/omega/Agentik_Engine/omega_engine/telegram_history.py +260 -0
- package/omega/Agentik_Engine/omega_engine/tmux.py +526 -0
- package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
- package/omega/Agentik_Engine/omega_engine/understand_anything.py +275 -0
- package/omega/Agentik_Engine/omega_engine/updater.py +70 -0
- package/omega/Agentik_Engine/omega_engine/validate.py +186 -0
- package/omega/Agentik_Engine/omega_engine/vault.py +342 -0
- package/omega/Agentik_Engine/omega_engine/webhooks.py +262 -0
- package/omega/Agentik_Engine/omega_engine/worker.py +526 -0
- package/omega/Agentik_Engine/pyproject.toml +1 -1
- package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/test_account.py +338 -0
- package/omega/Agentik_Engine/tests/test_adversarial.py +351 -0
- package/omega/Agentik_Engine/tests/test_agents_envelope.py +274 -0
- package/omega/Agentik_Engine/tests/test_audits_pipeline.py +348 -0
- package/omega/Agentik_Engine/tests/test_auto_update_and_migrations.py +394 -0
- package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
- package/omega/Agentik_Engine/tests/test_educators.py +233 -0
- package/omega/Agentik_Engine/tests/test_genesis_and_plan.py +573 -0
- package/omega/Agentik_Engine/tests/test_graphify.py +190 -0
- package/omega/Agentik_Engine/tests/test_handoff.py +311 -0
- package/omega/Agentik_Engine/tests/test_hermes_and_ua.py +387 -0
- package/omega/Agentik_Engine/tests/test_hermes_bootstrap_and_desktop.py +358 -0
- package/omega/Agentik_Engine/tests/test_install_steps.py +359 -0
- package/omega/Agentik_Engine/tests/test_install_ux.py +151 -0
- package/omega/Agentik_Engine/tests/test_installer_wiring.py +496 -0
- package/omega/Agentik_Engine/tests/test_intelligence.py +285 -0
- package/omega/Agentik_Engine/tests/test_llm_clis_and_uninstall.py +228 -0
- package/omega/Agentik_Engine/tests/test_managed_agent.py +363 -0
- package/omega/Agentik_Engine/tests/test_max_provider_and_menu.py +231 -0
- package/omega/Agentik_Engine/tests/test_menu_coverage.py +72 -0
- package/omega/Agentik_Engine/tests/test_pursue_cadence.py +217 -0
- package/omega/Agentik_Engine/tests/test_rag.py +287 -0
- package/omega/Agentik_Engine/tests/test_role_aliases_and_ssot.py +207 -0
- package/omega/Agentik_Engine/tests/test_skill_discovery_and_gate.py +337 -0
- package/omega/Agentik_Engine/tests/test_skill_power.py +259 -0
- package/omega/Agentik_Engine/tests/test_skill_routing.py +189 -0
- package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
- package/omega/Agentik_Engine/tests/test_telegram_history.py +209 -0
- package/omega/Agentik_Engine/tests/test_tmux_and_aisb_chat.py +223 -0
- package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
- package/omega/Agentik_Engine/tests/test_v06_features.py +370 -0
- package/omega/Agentik_Engine/tests/test_vault.py +173 -0
- package/omega/Agentik_Engine/tests/test_webhooks_and_readiness.py +277 -0
- package/omega/Agentik_Engine/tests/test_worker_and_cleanup.py +541 -0
- package/omega/Agentik_Extra/etc/secrets/.vault-key +3 -0
- package/omega/Agentik_Extra/etc/secrets/.vault-pub +1 -0
- package/omega/Agentik_Runtime/audits.db +0 -0
- package/omega/Agentik_SSOT/VERSION +1 -1
- package/omega/Agentik_SSOT/claude-plugins/claude-plugins.yaml +100 -0
- package/omega/Agentik_SSOT/docs/LAYERS.md +90 -0
- package/omega/Agentik_SSOT/docs/USER-JOURNEY.md +283 -0
- package/omega/Agentik_SSOT/marketplaces/design-discipline.yaml +86 -0
- package/omega/Agentik_SSOT/skills/a11yaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/apiaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/automationaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/cadence/SKILL.md +76 -0
- package/omega/Agentik_SSOT/skills/codeaudit/SKILL.md +153 -0
- package/omega/Agentik_SSOT/skills/copyaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/dataaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/debugaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/dispatch/SKILL.md +79 -0
- package/omega/Agentik_SSOT/skills/dxaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/featureaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/flowaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/genesis/SKILL.md +116 -0
- package/omega/Agentik_SSOT/skills/handoff/SKILL.md +117 -0
- package/omega/Agentik_SSOT/skills/logicaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/motionaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/perfaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/plan/SKILL.md +127 -0
- package/omega/Agentik_SSOT/skills/pursue/SKILL.md +68 -0
- package/omega/Agentik_SSOT/skills/rag-route.md +82 -0
- package/omega/Agentik_SSOT/skills/refontaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/retentionaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/secaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/seoaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/skill-auditor/SKILL.md +83 -0
- package/omega/Agentik_SSOT/skills/skill-finder/SKILL.md +116 -0
- package/omega/Agentik_SSOT/skills/uiuxaudit/SKILL.md +165 -0
- package/package.json +2 -2
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""The Claude Code Max account pool + per-account billing.
|
|
2
|
+
|
|
3
|
+
> Omega OS runs ONE engine process, not N tmux sessions. There is nothing to
|
|
4
|
+
> "switch" globally. The Claude provider holds a POOL and assigns each agent
|
|
5
|
+
> call to an account, so N Max accounts give the SUM of their rate limits as
|
|
6
|
+
> usable throughput. See `docs/ACCOUNT-AND-BILLING.md` for the design.
|
|
7
|
+
|
|
8
|
+
This module is the real implementation of that pool — plus the OAuth login
|
|
9
|
+
flow, the encrypted-vault file format, and the billing aggregator that scans
|
|
10
|
+
the event log for per-account token usage.
|
|
11
|
+
|
|
12
|
+
Stdlib only (yaml is already a top-level dep of omega-engine).
|
|
13
|
+
|
|
14
|
+
──── ON THE CLAUDE OAUTH DEVICE-CODE FLOW ────
|
|
15
|
+
|
|
16
|
+
Anthropic does **not** publicly document an RFC 8628 device-authorization-grant
|
|
17
|
+
endpoint for Claude Max accounts. The official Claude Code Max OAuth flow is
|
|
18
|
+
browser-based PKCE through `https://claude.ai`, which a headless VPS cannot
|
|
19
|
+
complete without launching a browser.
|
|
20
|
+
|
|
21
|
+
`claude_device_code_flow` therefore implements RFC 8628 correctly against
|
|
22
|
+
*configurable* endpoints (the spec is the spec — when Anthropic publishes the
|
|
23
|
+
URLs, the code already works). If the endpoint is unreachable or returns a
|
|
24
|
+
non-device-code response, the function raises with a clear message and the CLI
|
|
25
|
+
falls back to a **manual paste flow**: the user logs in via browser on another
|
|
26
|
+
machine, copies the OAuth token, pastes it into the prompt, and we write it
|
|
27
|
+
into the vault with `chmod 600`. That manual fallback is the path that always
|
|
28
|
+
works today — not a stub, a working credentials-into-the-vault flow.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import stat
|
|
35
|
+
import time
|
|
36
|
+
import urllib.error
|
|
37
|
+
import urllib.parse
|
|
38
|
+
import urllib.request
|
|
39
|
+
from dataclasses import dataclass, field, asdict
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
# yaml is a hard dependency of omega-engine — see pyproject.toml.
|
|
44
|
+
import yaml
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"ClaudeAccount",
|
|
49
|
+
"AccountPool",
|
|
50
|
+
"BillingAggregator",
|
|
51
|
+
"vault_path",
|
|
52
|
+
"read_token",
|
|
53
|
+
"write_token",
|
|
54
|
+
"claude_oauth_login_url",
|
|
55
|
+
"claude_device_code_flow",
|
|
56
|
+
"ACCOUNT_DEVICE_AUTH_URL",
|
|
57
|
+
"ACCOUNT_DEVICE_TOKEN_URL",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ──── OAuth endpoint constants ────
|
|
62
|
+
# Configurable via env so tests / future Anthropic endpoint changes do not
|
|
63
|
+
# require a code edit. Defaults reflect the documented RFC 8628 shape; if
|
|
64
|
+
# Anthropic publishes the actual URLs, set them with these env vars (or update
|
|
65
|
+
# the constants here in one place).
|
|
66
|
+
ACCOUNT_DEVICE_AUTH_URL = os.environ.get(
|
|
67
|
+
"OMEGA_CLAUDE_DEVICE_AUTH_URL",
|
|
68
|
+
"https://auth.anthropic.com/oauth/device/authorize",
|
|
69
|
+
)
|
|
70
|
+
ACCOUNT_DEVICE_TOKEN_URL = os.environ.get(
|
|
71
|
+
"OMEGA_CLAUDE_DEVICE_TOKEN_URL",
|
|
72
|
+
"https://auth.anthropic.com/oauth/device/token",
|
|
73
|
+
)
|
|
74
|
+
ACCOUNT_BROWSER_LOGIN_URL = "https://claude.ai/oauth"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ──── data model ────
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ClaudeAccount:
|
|
82
|
+
"""One Claude Code Max account entry in the pool.
|
|
83
|
+
|
|
84
|
+
`secret_ref` is the only handle to the OAuth token — the token itself
|
|
85
|
+
lives in `Agentik_Extra/etc/secrets/<secret_ref>.env` with mode 600.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
id: str
|
|
89
|
+
label: str = ""
|
|
90
|
+
secret_ref: str = ""
|
|
91
|
+
weight: int = 1
|
|
92
|
+
status: str = "active" # active | resting | disabled
|
|
93
|
+
last_used_at: float = 0.0
|
|
94
|
+
tokens_used: int = 0
|
|
95
|
+
|
|
96
|
+
def to_yaml(self) -> dict[str, Any]:
|
|
97
|
+
"""Serialize for accounts.yaml. Skips zero-valued runtime fields so the
|
|
98
|
+
on-disk file stays clean for accounts that have never been used."""
|
|
99
|
+
out: dict[str, Any] = {
|
|
100
|
+
"id": self.id,
|
|
101
|
+
"label": self.label,
|
|
102
|
+
"secret_ref": self.secret_ref,
|
|
103
|
+
"weight": int(self.weight),
|
|
104
|
+
"status": self.status,
|
|
105
|
+
}
|
|
106
|
+
if self.last_used_at:
|
|
107
|
+
out["last_used_at"] = float(self.last_used_at)
|
|
108
|
+
if self.tokens_used:
|
|
109
|
+
out["tokens_used"] = int(self.tokens_used)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_yaml(cls, row: dict[str, Any]) -> "ClaudeAccount":
|
|
114
|
+
return cls(
|
|
115
|
+
id=str(row.get("id", "")).strip(),
|
|
116
|
+
label=str(row.get("label", "")),
|
|
117
|
+
secret_ref=str(row.get("secret_ref", "")),
|
|
118
|
+
weight=int(row.get("weight", 1)),
|
|
119
|
+
status=str(row.get("status", "active")),
|
|
120
|
+
last_used_at=float(row.get("last_used_at", 0.0) or 0.0),
|
|
121
|
+
tokens_used=int(row.get("tokens_used", 0) or 0),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class AccountPool:
|
|
127
|
+
"""The pool of Claude Code Max accounts.
|
|
128
|
+
|
|
129
|
+
`selection` controls how `next()` picks the account for the next call:
|
|
130
|
+
- `round-robin` — strict rotation among active accounts
|
|
131
|
+
- `least-used` — pick the active account with the smallest `tokens_used`
|
|
132
|
+
- `by-quota` — like least-used but weighted by `weight`
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
accounts: list[ClaudeAccount] = field(default_factory=list)
|
|
136
|
+
selection: str = "least-used"
|
|
137
|
+
version: int = 1
|
|
138
|
+
_rr_cursor: int = 0 # private; used by round-robin
|
|
139
|
+
|
|
140
|
+
# ──── loading & saving ────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def load(cls, omega_home: str | Path) -> "AccountPool":
|
|
144
|
+
"""Load the pool. Prefers `accounts.yaml`, falls back to
|
|
145
|
+
`accounts.example.yaml`. Returns an empty pool if neither exists."""
|
|
146
|
+
home = Path(omega_home)
|
|
147
|
+
cfg_dir = home / "Agentik_Providers" / "claude"
|
|
148
|
+
for name in ("accounts.yaml", "accounts.example.yaml"):
|
|
149
|
+
cfg = cfg_dir / name
|
|
150
|
+
if cfg.exists():
|
|
151
|
+
return cls._from_file(cfg)
|
|
152
|
+
return cls()
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def _from_file(cls, path: Path) -> "AccountPool":
|
|
156
|
+
raw = yaml.safe_load(path.read_text()) or {}
|
|
157
|
+
accounts = [ClaudeAccount.from_yaml(r) for r in (raw.get("pool") or [])]
|
|
158
|
+
return cls(
|
|
159
|
+
accounts=accounts,
|
|
160
|
+
selection=str(raw.get("selection", "least-used")),
|
|
161
|
+
version=int(raw.get("version", 1) or 1),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def save(self, omega_home: str | Path) -> Path:
|
|
165
|
+
"""Persist the pool to `accounts.yaml` (creates parent dirs).
|
|
166
|
+
|
|
167
|
+
Always writes to the canonical name (`accounts.yaml`), never to the
|
|
168
|
+
example file — that one is the template that ships with the repo.
|
|
169
|
+
"""
|
|
170
|
+
home = Path(omega_home)
|
|
171
|
+
cfg_dir = home / "Agentik_Providers" / "claude"
|
|
172
|
+
cfg_dir.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
cfg = cfg_dir / "accounts.yaml"
|
|
174
|
+
data = {
|
|
175
|
+
"version": self.version,
|
|
176
|
+
"selection": self.selection,
|
|
177
|
+
"pool": [a.to_yaml() for a in self.accounts],
|
|
178
|
+
}
|
|
179
|
+
# default_flow_style=False keeps it human-readable; sort_keys=False
|
|
180
|
+
# preserves the natural field order (id, label, secret_ref, ...).
|
|
181
|
+
cfg.write_text(yaml.safe_dump(data, sort_keys=False, default_flow_style=False))
|
|
182
|
+
return cfg
|
|
183
|
+
|
|
184
|
+
# ──── selection ───────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def _active(self) -> list[ClaudeAccount]:
|
|
187
|
+
return [a for a in self.accounts if a.status == "active"]
|
|
188
|
+
|
|
189
|
+
def next(self) -> ClaudeAccount:
|
|
190
|
+
"""Pick the next account per the current `selection` strategy.
|
|
191
|
+
|
|
192
|
+
Raises RuntimeError if no account is active — that's a real error the
|
|
193
|
+
caller (the Claude provider) must surface, not silently mock out.
|
|
194
|
+
"""
|
|
195
|
+
active = self._active()
|
|
196
|
+
if not active:
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
"no active Claude Max accounts in the pool — "
|
|
199
|
+
"run `omega account login` or `omega account use <id> active`"
|
|
200
|
+
)
|
|
201
|
+
if self.selection == "round-robin":
|
|
202
|
+
chosen = active[self._rr_cursor % len(active)]
|
|
203
|
+
self._rr_cursor = (self._rr_cursor + 1) % len(active)
|
|
204
|
+
elif self.selection == "by-quota":
|
|
205
|
+
# weighted least-used: score = tokens_used / max(weight, 1).
|
|
206
|
+
# smallest score wins. ties broken by last_used_at (older first).
|
|
207
|
+
chosen = min(
|
|
208
|
+
active,
|
|
209
|
+
key=lambda a: (a.tokens_used / max(a.weight, 1), a.last_used_at),
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
# least-used (default): smallest tokens_used, then oldest last_used.
|
|
213
|
+
chosen = min(active, key=lambda a: (a.tokens_used, a.last_used_at))
|
|
214
|
+
chosen.last_used_at = time.time()
|
|
215
|
+
return chosen
|
|
216
|
+
|
|
217
|
+
# ──── mutation helpers ────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def add(self, account: ClaudeAccount) -> None:
|
|
220
|
+
"""Add or replace (by id) an account in the pool."""
|
|
221
|
+
if not account.id:
|
|
222
|
+
raise ValueError("account.id must be set")
|
|
223
|
+
existing = self.get(account.id)
|
|
224
|
+
if existing is None:
|
|
225
|
+
self.accounts.append(account)
|
|
226
|
+
else:
|
|
227
|
+
# replace in place so list ordering is preserved
|
|
228
|
+
idx = self.accounts.index(existing)
|
|
229
|
+
self.accounts[idx] = account
|
|
230
|
+
|
|
231
|
+
def get(self, account_id: str) -> ClaudeAccount | None:
|
|
232
|
+
for a in self.accounts:
|
|
233
|
+
if a.id == account_id:
|
|
234
|
+
return a
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def set_status(self, account_id: str, status: str) -> None:
|
|
238
|
+
if status not in ("active", "resting", "disabled"):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"invalid status '{status}' — must be active|resting|disabled"
|
|
241
|
+
)
|
|
242
|
+
acc = self.get(account_id)
|
|
243
|
+
if acc is None:
|
|
244
|
+
raise KeyError(f"no account '{account_id}' in the pool")
|
|
245
|
+
acc.status = status
|
|
246
|
+
|
|
247
|
+
def usage_for(self, account_id: str, tokens: int) -> None:
|
|
248
|
+
"""Record `tokens` usage on `account_id`. Idempotent at the pool level
|
|
249
|
+
(the event log is the source of truth — this just updates the cached
|
|
250
|
+
counter shown by `omega account list`)."""
|
|
251
|
+
acc = self.get(account_id)
|
|
252
|
+
if acc is None:
|
|
253
|
+
return
|
|
254
|
+
acc.tokens_used += max(int(tokens), 0)
|
|
255
|
+
acc.last_used_at = time.time()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ──── vault: where the OAuth tokens actually live ────────────────────────
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def vault_path(omega_home: str | Path, secret_ref: str) -> Path:
|
|
262
|
+
"""Return the on-disk path for a secret reference.
|
|
263
|
+
|
|
264
|
+
Convention: `Agentik_Extra/etc/secrets/<secret_ref>.env`. The directory is
|
|
265
|
+
created on demand with mode 700; individual secret files are mode 600.
|
|
266
|
+
|
|
267
|
+
Refuses path-traversal — `secret_ref` cannot contain `/` or `..`.
|
|
268
|
+
"""
|
|
269
|
+
if not secret_ref:
|
|
270
|
+
raise ValueError("secret_ref must not be empty")
|
|
271
|
+
if "/" in secret_ref or "\\" in secret_ref or ".." in secret_ref:
|
|
272
|
+
raise ValueError(f"secret_ref must be a flat name, got {secret_ref!r}")
|
|
273
|
+
home = Path(omega_home)
|
|
274
|
+
secrets_dir = home / "Agentik_Extra" / "etc" / "secrets"
|
|
275
|
+
secrets_dir.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
try:
|
|
277
|
+
os.chmod(secrets_dir, 0o700)
|
|
278
|
+
except (OSError, PermissionError):
|
|
279
|
+
# best-effort — file mode is enforced below where it matters most
|
|
280
|
+
pass
|
|
281
|
+
return secrets_dir / f"{secret_ref}.env"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def read_token(omega_home: str | Path, secret_ref: str) -> str:
|
|
285
|
+
"""Read the OAuth token from the vault.
|
|
286
|
+
|
|
287
|
+
Resolution order, first match wins:
|
|
288
|
+
1. ``vault_read(<secret_ref>_TOKEN)`` — the canonical key the new
|
|
289
|
+
``write_token`` uses. If age is the active backend the read is
|
|
290
|
+
transparently decrypted.
|
|
291
|
+
2. Legacy dotenv file at ``Agentik_Extra/etc/secrets/<secret_ref>.env``
|
|
292
|
+
with a ``CLAUDE_OAUTH_TOKEN=...`` line. Kept for backward compat
|
|
293
|
+
with installs that pre-date the encrypted vault.
|
|
294
|
+
"""
|
|
295
|
+
# Local import to avoid an import cycle at module load time.
|
|
296
|
+
from omega_engine.vault import vault_read
|
|
297
|
+
|
|
298
|
+
canon = f"{secret_ref}_TOKEN"
|
|
299
|
+
try:
|
|
300
|
+
token = vault_read(omega_home, canon)
|
|
301
|
+
except ValueError:
|
|
302
|
+
token = None
|
|
303
|
+
if token:
|
|
304
|
+
return token.strip()
|
|
305
|
+
|
|
306
|
+
path = vault_path(omega_home, secret_ref)
|
|
307
|
+
if not path.exists():
|
|
308
|
+
raise FileNotFoundError(f"vault entry not found for ref {secret_ref!r}")
|
|
309
|
+
keys = ("CLAUDE_OAUTH_TOKEN", secret_ref.upper())
|
|
310
|
+
for line in path.read_text().splitlines():
|
|
311
|
+
s = line.strip()
|
|
312
|
+
if not s or s.startswith("#") or "=" not in s:
|
|
313
|
+
continue
|
|
314
|
+
k, _, v = s.partition("=")
|
|
315
|
+
k = k.strip()
|
|
316
|
+
v = v.strip().strip('"').strip("'")
|
|
317
|
+
if k in keys and v:
|
|
318
|
+
return v
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"no CLAUDE_OAUTH_TOKEN in vault file {path} — "
|
|
321
|
+
"expected a line like `CLAUDE_OAUTH_TOKEN=...`"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def write_token(omega_home: str | Path, secret_ref: str, token: str) -> Path:
|
|
326
|
+
"""Write the OAuth token through the vault layer.
|
|
327
|
+
|
|
328
|
+
Uses ``omega_engine.vault.vault_write``: age-encrypted if the system has
|
|
329
|
+
``age`` + ``age-keygen``, else mode-600 plaintext. Either way, the file
|
|
330
|
+
is owner-only on disk. Overwrites silently — rotating a token is the
|
|
331
|
+
expected use case. Returns the actual on-disk path written.
|
|
332
|
+
"""
|
|
333
|
+
if not token or not token.strip():
|
|
334
|
+
raise ValueError("token must be a non-empty string")
|
|
335
|
+
from omega_engine.vault import vault_write
|
|
336
|
+
return vault_write(omega_home, f"{secret_ref}_TOKEN", token.strip())
|
|
337
|
+
return path
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ──── OAuth login flow ───────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def claude_oauth_login_url() -> str:
|
|
344
|
+
"""The URL a human visits to obtain a Claude OAuth token.
|
|
345
|
+
|
|
346
|
+
Returned as a plain string so the CLI can print it and instruct the user.
|
|
347
|
+
"""
|
|
348
|
+
return ACCOUNT_BROWSER_LOGIN_URL
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def claude_device_code_flow(
|
|
352
|
+
client_id: str = "claude-code-cli",
|
|
353
|
+
scope: str = "claude:max",
|
|
354
|
+
device_auth_url: str | None = None,
|
|
355
|
+
token_url: str | None = None,
|
|
356
|
+
poll_timeout: float = 600.0,
|
|
357
|
+
) -> dict[str, Any]:
|
|
358
|
+
"""RFC 8628 device authorization flow against the Claude OAuth endpoints.
|
|
359
|
+
|
|
360
|
+
Returns a dict with at least `access_token`. If the endpoint does not
|
|
361
|
+
support this flow (404 / 4xx / non-JSON), raises RuntimeError with a clear
|
|
362
|
+
message so the CLI knows to fall back to manual paste.
|
|
363
|
+
|
|
364
|
+
This function performs ZERO disk I/O — the CLI is responsible for writing
|
|
365
|
+
the returned token into the vault. Keeps the function pure and testable.
|
|
366
|
+
"""
|
|
367
|
+
auth_url = device_auth_url or ACCOUNT_DEVICE_AUTH_URL
|
|
368
|
+
tok_url = token_url or ACCOUNT_DEVICE_TOKEN_URL
|
|
369
|
+
|
|
370
|
+
# ---- step 1: request a device_code ----
|
|
371
|
+
body = urllib.parse.urlencode({"client_id": client_id, "scope": scope}).encode()
|
|
372
|
+
req = urllib.request.Request(
|
|
373
|
+
auth_url, data=body, method="POST",
|
|
374
|
+
headers={"Content-Type": "application/x-www-form-urlencoded",
|
|
375
|
+
"Accept": "application/json"},
|
|
376
|
+
)
|
|
377
|
+
try:
|
|
378
|
+
with urllib.request.urlopen(req, timeout=30.0) as resp:
|
|
379
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
380
|
+
except urllib.error.HTTPError as exc:
|
|
381
|
+
raise RuntimeError(
|
|
382
|
+
f"device authorize endpoint returned HTTP {exc.code}. "
|
|
383
|
+
"Anthropic may not support the device-code flow yet — "
|
|
384
|
+
"fall back to manual paste."
|
|
385
|
+
) from exc
|
|
386
|
+
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as exc:
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
f"could not reach the device authorize endpoint: {exc}. "
|
|
389
|
+
"Fall back to manual paste."
|
|
390
|
+
) from exc
|
|
391
|
+
|
|
392
|
+
device_code = payload.get("device_code")
|
|
393
|
+
interval = float(payload.get("interval", 5))
|
|
394
|
+
expires_in = float(payload.get("expires_in", 600))
|
|
395
|
+
if not device_code:
|
|
396
|
+
raise RuntimeError(
|
|
397
|
+
f"device authorize response missing `device_code`: {payload!r}. "
|
|
398
|
+
"Fall back to manual paste."
|
|
399
|
+
)
|
|
400
|
+
verification = (
|
|
401
|
+
payload.get("verification_uri_complete")
|
|
402
|
+
or payload.get("verification_uri")
|
|
403
|
+
or "(see provider docs)"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# The caller (CLI) prints this — but in case it's invoked headlessly, surface
|
|
407
|
+
# it on the returned payload too.
|
|
408
|
+
payload["_human_instruction"] = (
|
|
409
|
+
f"open in a browser and approve:\n {verification}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# ---- step 2: poll for the token ----
|
|
413
|
+
deadline = min(time.monotonic() + poll_timeout, time.monotonic() + expires_in)
|
|
414
|
+
poll_body = urllib.parse.urlencode({
|
|
415
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
416
|
+
"device_code": device_code,
|
|
417
|
+
"client_id": client_id,
|
|
418
|
+
}).encode()
|
|
419
|
+
while time.monotonic() < deadline:
|
|
420
|
+
time.sleep(max(interval, 1.0))
|
|
421
|
+
poll_req = urllib.request.Request(
|
|
422
|
+
tok_url, data=poll_body, method="POST",
|
|
423
|
+
headers={"Content-Type": "application/x-www-form-urlencoded",
|
|
424
|
+
"Accept": "application/json"},
|
|
425
|
+
)
|
|
426
|
+
try:
|
|
427
|
+
with urllib.request.urlopen(poll_req, timeout=30.0) as resp:
|
|
428
|
+
tok = json.loads(resp.read().decode("utf-8"))
|
|
429
|
+
except urllib.error.HTTPError as exc:
|
|
430
|
+
try:
|
|
431
|
+
err_payload = json.loads(exc.read().decode("utf-8"))
|
|
432
|
+
except Exception: # noqa: BLE001 — best effort on body decode
|
|
433
|
+
err_payload = {}
|
|
434
|
+
err = err_payload.get("error", "")
|
|
435
|
+
# RFC 8628 §3.5: authorization_pending and slow_down mean keep polling.
|
|
436
|
+
if err == "authorization_pending":
|
|
437
|
+
continue
|
|
438
|
+
if err == "slow_down":
|
|
439
|
+
interval += 5
|
|
440
|
+
continue
|
|
441
|
+
raise RuntimeError(
|
|
442
|
+
f"device token endpoint refused: {err or exc.code} — "
|
|
443
|
+
f"{err_payload!r}"
|
|
444
|
+
) from exc
|
|
445
|
+
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as exc:
|
|
446
|
+
raise RuntimeError(f"could not poll token endpoint: {exc}") from exc
|
|
447
|
+
|
|
448
|
+
if tok.get("access_token"):
|
|
449
|
+
return {
|
|
450
|
+
"access_token": tok["access_token"],
|
|
451
|
+
"refresh_token": tok.get("refresh_token", ""),
|
|
452
|
+
"expires_in": int(tok.get("expires_in", 0) or 0),
|
|
453
|
+
"raw": tok,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
raise RuntimeError(
|
|
457
|
+
"device-code flow timed out — user did not approve in time"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ──── billing aggregator ─────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class BillingAggregator:
|
|
465
|
+
"""Aggregate per-account token usage from the event log.
|
|
466
|
+
|
|
467
|
+
Scans every `task.*` event whose payload includes a `usage.account_id` and
|
|
468
|
+
a positive `usage.input_tokens` / `usage.output_tokens`. The event log is
|
|
469
|
+
the source of truth — `AccountPool.tokens_used` is a cached view.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def from_event_log(store: Any) -> dict[str, dict[str, int]]:
|
|
474
|
+
"""Walk the store and aggregate. `store` is anything that yields
|
|
475
|
+
Event-like objects via `.all_events()` (see `omega_engine.store`).
|
|
476
|
+
|
|
477
|
+
Returns: { account_id: { input_tokens, output_tokens, total_calls } }
|
|
478
|
+
"""
|
|
479
|
+
totals: dict[str, dict[str, int]] = {}
|
|
480
|
+
for ev in store.all_events():
|
|
481
|
+
# only billable events
|
|
482
|
+
etype = getattr(ev, "type", None)
|
|
483
|
+
type_val = etype.value if hasattr(etype, "value") else str(etype)
|
|
484
|
+
if not type_val.startswith("task."):
|
|
485
|
+
continue
|
|
486
|
+
payload = getattr(ev, "payload", None) or {}
|
|
487
|
+
usage = payload.get("usage") or {}
|
|
488
|
+
account_id = usage.get("account_id")
|
|
489
|
+
if not account_id:
|
|
490
|
+
continue
|
|
491
|
+
inp = int(usage.get("input_tokens", 0) or 0)
|
|
492
|
+
out = int(usage.get("output_tokens", 0) or 0)
|
|
493
|
+
if inp <= 0 and out <= 0:
|
|
494
|
+
continue
|
|
495
|
+
slot = totals.setdefault(
|
|
496
|
+
account_id,
|
|
497
|
+
{"input_tokens": 0, "output_tokens": 0, "total_calls": 0},
|
|
498
|
+
)
|
|
499
|
+
slot["input_tokens"] += inp
|
|
500
|
+
slot["output_tokens"] += out
|
|
501
|
+
slot["total_calls"] += 1
|
|
502
|
+
return totals
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Worker ↔ Oracle messaging — the missing back-channel.
|
|
2
|
+
|
|
3
|
+
Today a worker writes a single ``.done.json`` and that's it. If it's
|
|
4
|
+
stuck mid-task and needs the oracle's input, it has no way to ask. Then
|
|
5
|
+
either it guesses wrong (and the audit catches it later — wasted work)
|
|
6
|
+
or it gives up early (status=pending — wasted dispatch).
|
|
7
|
+
|
|
8
|
+
This primitive is a small JSON-lines file per task at
|
|
9
|
+
``Agentik_Runtime/sessions/<task_id>/messages.jsonl``. The worker
|
|
10
|
+
appends a question, the oracle reads, appends an answer, the worker
|
|
11
|
+
polls and continues.
|
|
12
|
+
|
|
13
|
+
The protocol is intentionally minimal — one JSON object per line:
|
|
14
|
+
|
|
15
|
+
``{from, to, ts, type, text}``
|
|
16
|
+
|
|
17
|
+
``type`` is one of:
|
|
18
|
+
|
|
19
|
+
* ``question`` — worker asks the oracle
|
|
20
|
+
* ``answer`` — oracle's reply
|
|
21
|
+
* ``proposal`` — worker proposes a course change
|
|
22
|
+
* ``decision`` — oracle says yes/no/pivot
|
|
23
|
+
|
|
24
|
+
Polling is the operator's choice — the worker can ``read_messages``
|
|
25
|
+
between Bash/Edit iterations. For long-running missions the oracle's
|
|
26
|
+
session can sit in ``omega message wait <task_id>`` to surface
|
|
27
|
+
questions as they arrive.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
VALID_TYPES = {"question", "answer", "proposal", "decision"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Message:
|
|
43
|
+
from_: str
|
|
44
|
+
to: str
|
|
45
|
+
type: str
|
|
46
|
+
text: str
|
|
47
|
+
ts: int = field(default_factory=lambda: int(time.time()))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _msg_path(omega_home: str | Path, task_id: str) -> Path:
|
|
51
|
+
home = Path(omega_home)
|
|
52
|
+
p = home / "Agentik_Runtime" / "sessions" / task_id / "messages.jsonl"
|
|
53
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def send(
|
|
58
|
+
omega_home: str | Path,
|
|
59
|
+
task_id: str,
|
|
60
|
+
*,
|
|
61
|
+
from_: str,
|
|
62
|
+
to: str,
|
|
63
|
+
type: str,
|
|
64
|
+
text: str,
|
|
65
|
+
) -> Message:
|
|
66
|
+
"""Append one message to the per-task JSONL. Returns the persisted Message."""
|
|
67
|
+
if type not in VALID_TYPES:
|
|
68
|
+
raise ValueError(f"type must be one of {VALID_TYPES}, got {type!r}")
|
|
69
|
+
msg = Message(from_=from_, to=to, type=type, text=text)
|
|
70
|
+
p = _msg_path(omega_home, task_id)
|
|
71
|
+
with p.open("a") as fh:
|
|
72
|
+
fh.write(json.dumps({
|
|
73
|
+
"from": msg.from_, "to": msg.to,
|
|
74
|
+
"type": msg.type, "text": msg.text,
|
|
75
|
+
"ts": msg.ts,
|
|
76
|
+
}) + "\n")
|
|
77
|
+
return msg
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read(
|
|
81
|
+
omega_home: str | Path, task_id: str,
|
|
82
|
+
*,
|
|
83
|
+
since_ts: int = 0,
|
|
84
|
+
for_recipient: str | None = None,
|
|
85
|
+
) -> list[Message]:
|
|
86
|
+
"""Read messages for a task. Returns oldest-first."""
|
|
87
|
+
p = _msg_path(omega_home, task_id)
|
|
88
|
+
if not p.exists():
|
|
89
|
+
return []
|
|
90
|
+
out: list[Message] = []
|
|
91
|
+
for line in p.read_text().splitlines():
|
|
92
|
+
if not line.strip():
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
data = json.loads(line)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
continue
|
|
98
|
+
if int(data.get("ts", 0)) < since_ts:
|
|
99
|
+
continue
|
|
100
|
+
if for_recipient and data.get("to") != for_recipient:
|
|
101
|
+
continue
|
|
102
|
+
out.append(Message(
|
|
103
|
+
from_=str(data.get("from", "?")),
|
|
104
|
+
to=str(data.get("to", "?")),
|
|
105
|
+
type=str(data.get("type", "question")),
|
|
106
|
+
text=str(data.get("text", "")),
|
|
107
|
+
ts=int(data.get("ts", 0)),
|
|
108
|
+
))
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def wait_for_reply(
|
|
113
|
+
omega_home: str | Path,
|
|
114
|
+
task_id: str,
|
|
115
|
+
*,
|
|
116
|
+
for_recipient: str,
|
|
117
|
+
since_ts: int | None = None,
|
|
118
|
+
timeout_s: int = 600,
|
|
119
|
+
poll_interval_s: int = 5,
|
|
120
|
+
) -> Message | None:
|
|
121
|
+
"""Block until a message addressed to ``for_recipient`` arrives or timeout.
|
|
122
|
+
|
|
123
|
+
Use case: the worker asks a question, then waits for the oracle's
|
|
124
|
+
answer before continuing. Returns the new message, or None on
|
|
125
|
+
timeout.
|
|
126
|
+
"""
|
|
127
|
+
since_ts = since_ts if since_ts is not None else int(time.time())
|
|
128
|
+
end = time.time() + timeout_s
|
|
129
|
+
while time.time() < end:
|
|
130
|
+
msgs = read(omega_home, task_id, since_ts=since_ts,
|
|
131
|
+
for_recipient=for_recipient)
|
|
132
|
+
if msgs:
|
|
133
|
+
return msgs[0]
|
|
134
|
+
time.sleep(poll_interval_s)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def list_open_questions(
|
|
139
|
+
omega_home: str | Path,
|
|
140
|
+
) -> list[tuple[str, Message]]:
|
|
141
|
+
"""Scan every per-task messages.jsonl for unanswered questions.
|
|
142
|
+
|
|
143
|
+
A "question" is unanswered when no later message of type=answer
|
|
144
|
+
addresses the same conversation. Returns (task_id, question_msg)
|
|
145
|
+
pairs sorted oldest first.
|
|
146
|
+
"""
|
|
147
|
+
home = Path(omega_home)
|
|
148
|
+
sess = home / "Agentik_Runtime" / "sessions"
|
|
149
|
+
if not sess.is_dir():
|
|
150
|
+
return []
|
|
151
|
+
open_qs: list[tuple[str, Message]] = []
|
|
152
|
+
for child in sess.iterdir():
|
|
153
|
+
if not child.is_dir():
|
|
154
|
+
continue
|
|
155
|
+
p = child / "messages.jsonl"
|
|
156
|
+
if not p.exists():
|
|
157
|
+
continue
|
|
158
|
+
msgs = read(home, child.name)
|
|
159
|
+
# Questions whose ts is later than the latest answer in the same file
|
|
160
|
+
latest_answer = max(
|
|
161
|
+
(m.ts for m in msgs if m.type == "answer"), default=0,
|
|
162
|
+
)
|
|
163
|
+
for m in msgs:
|
|
164
|
+
if m.type == "question" and m.ts > latest_answer:
|
|
165
|
+
open_qs.append((child.name, m))
|
|
166
|
+
open_qs.sort(key=lambda pair: pair[1].ts)
|
|
167
|
+
return open_qs
|