@smilintux/skcapstone 0.1.0 → 0.2.3
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/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""Sovereign agent state export and import.
|
|
2
|
+
|
|
3
|
+
Produces a portable JSON bundle containing the full agent state:
|
|
4
|
+
identity, soul, memories, conversations, and config. Suitable for
|
|
5
|
+
migrating an agent to a new machine or sharing a snapshot.
|
|
6
|
+
|
|
7
|
+
Bundle schema (``bundle_version: 1``):
|
|
8
|
+
|
|
9
|
+
.. code-block:: json
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"bundle_version": 1,
|
|
13
|
+
"exported_at": "<ISO-8601>",
|
|
14
|
+
"agent_name": "opus",
|
|
15
|
+
"skcapstone_version": "0.9.0",
|
|
16
|
+
"identity": { ... },
|
|
17
|
+
"config": { ... },
|
|
18
|
+
"soul": {
|
|
19
|
+
"base": { ... },
|
|
20
|
+
"active": { ... },
|
|
21
|
+
"installed": { "soul-name": { ... } }
|
|
22
|
+
},
|
|
23
|
+
"memories": [ { "memory_id": ..., "content": ..., ... } ],
|
|
24
|
+
"conversations": { "peer-name": [ { "role": ..., "content": ..., "timestamp": ... } ] }
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Optional
|
|
35
|
+
|
|
36
|
+
import yaml
|
|
37
|
+
|
|
38
|
+
from . import __version__
|
|
39
|
+
from .memory_engine import list_memories, store as memory_store
|
|
40
|
+
from .models import MemoryLayer
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger("skcapstone.export")
|
|
43
|
+
|
|
44
|
+
BUNDLE_VERSION = 1
|
|
45
|
+
|
|
46
|
+
# Files relative to home that contain identity data
|
|
47
|
+
_IDENTITY_FILE = "identity/identity.json"
|
|
48
|
+
_CONFIG_FILE = "config/config.yaml"
|
|
49
|
+
_SOUL_BASE = "soul/base.json"
|
|
50
|
+
_SOUL_ACTIVE = "soul/active.json"
|
|
51
|
+
_SOUL_INSTALLED_DIR = "soul/installed"
|
|
52
|
+
_CONVERSATIONS_DIR = "conversations"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def export_bundle(home: Path) -> dict[str, Any]:
|
|
56
|
+
"""Export the full agent state as a portable JSON-serializable bundle.
|
|
57
|
+
|
|
58
|
+
Collects identity, config, soul overlays, all memories, and all
|
|
59
|
+
conversation histories from the agent home directory. Missing
|
|
60
|
+
sections are included as empty dicts/lists rather than raising.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
home: Agent home directory (e.g. ``~/.skcapstone``).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict: Fully serializable bundle, ready for ``json.dumps``.
|
|
67
|
+
"""
|
|
68
|
+
home = Path(home).expanduser()
|
|
69
|
+
|
|
70
|
+
bundle: dict[str, Any] = {
|
|
71
|
+
"bundle_version": BUNDLE_VERSION,
|
|
72
|
+
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
73
|
+
"agent_name": _read_agent_name(home),
|
|
74
|
+
"skcapstone_version": __version__,
|
|
75
|
+
"identity": _load_identity(home),
|
|
76
|
+
"config": _load_config(home),
|
|
77
|
+
"soul": _load_soul(home),
|
|
78
|
+
"memories": _load_memories(home),
|
|
79
|
+
"conversations": _load_conversations(home),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
"Exported bundle for %s: %d memories, %d conversations",
|
|
84
|
+
bundle["agent_name"],
|
|
85
|
+
len(bundle["memories"]),
|
|
86
|
+
len(bundle["conversations"]),
|
|
87
|
+
)
|
|
88
|
+
return bundle
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def import_bundle(
|
|
92
|
+
home: Path,
|
|
93
|
+
bundle: dict[str, Any],
|
|
94
|
+
overwrite_identity: bool = False,
|
|
95
|
+
overwrite_config: bool = False,
|
|
96
|
+
overwrite_soul: bool = False,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Import an agent state bundle into the target home directory.
|
|
99
|
+
|
|
100
|
+
Memories are imported using duplicate-ID detection (existing memories
|
|
101
|
+
are never overwritten). Conversations are merged per-peer, appending
|
|
102
|
+
only messages not already present. Identity, config, and soul are
|
|
103
|
+
written only when the corresponding flag is set or the file is absent.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
home: Target agent home directory.
|
|
107
|
+
bundle: Bundle dict as produced by :func:`export_bundle`.
|
|
108
|
+
overwrite_identity: Overwrite ``identity/identity.json`` even if
|
|
109
|
+
the file already exists.
|
|
110
|
+
overwrite_config: Overwrite ``config/config.yaml`` even if it
|
|
111
|
+
already exists.
|
|
112
|
+
overwrite_soul: Overwrite soul files even if they already exist.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: Import summary with keys ``memories_imported``,
|
|
116
|
+
``conversations_imported``, ``identity_written``,
|
|
117
|
+
``config_written``, ``soul_files_written``, ``errors``.
|
|
118
|
+
"""
|
|
119
|
+
home = Path(home).expanduser()
|
|
120
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
_validate_bundle(bundle)
|
|
123
|
+
|
|
124
|
+
errors: list[str] = []
|
|
125
|
+
summary: dict[str, Any] = {
|
|
126
|
+
"memories_imported": 0,
|
|
127
|
+
"conversations_imported": 0,
|
|
128
|
+
"identity_written": False,
|
|
129
|
+
"config_written": False,
|
|
130
|
+
"soul_files_written": 0,
|
|
131
|
+
"errors": errors,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# --- identity ---
|
|
135
|
+
identity_path = home / _IDENTITY_FILE
|
|
136
|
+
if bundle.get("identity") and (overwrite_identity or not identity_path.exists()):
|
|
137
|
+
try:
|
|
138
|
+
identity_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
identity_path.write_text(
|
|
140
|
+
json.dumps(bundle["identity"], indent=2), encoding="utf-8"
|
|
141
|
+
)
|
|
142
|
+
summary["identity_written"] = True
|
|
143
|
+
except OSError as exc:
|
|
144
|
+
errors.append(f"identity write failed: {exc}")
|
|
145
|
+
|
|
146
|
+
# --- config ---
|
|
147
|
+
config_path = home / _CONFIG_FILE
|
|
148
|
+
if bundle.get("config") and (overwrite_config or not config_path.exists()):
|
|
149
|
+
try:
|
|
150
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
config_path.write_text(
|
|
152
|
+
yaml.safe_dump(bundle["config"], default_flow_style=False),
|
|
153
|
+
encoding="utf-8",
|
|
154
|
+
)
|
|
155
|
+
summary["config_written"] = True
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
errors.append(f"config write failed: {exc}")
|
|
158
|
+
|
|
159
|
+
# --- soul ---
|
|
160
|
+
soul_section = bundle.get("soul") or {}
|
|
161
|
+
soul_written = 0
|
|
162
|
+
|
|
163
|
+
base_path = home / _SOUL_BASE
|
|
164
|
+
if soul_section.get("base") and (overwrite_soul or not base_path.exists()):
|
|
165
|
+
try:
|
|
166
|
+
base_path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
base_path.write_text(
|
|
168
|
+
json.dumps(soul_section["base"], indent=2), encoding="utf-8"
|
|
169
|
+
)
|
|
170
|
+
soul_written += 1
|
|
171
|
+
except OSError as exc:
|
|
172
|
+
errors.append(f"soul/base write failed: {exc}")
|
|
173
|
+
|
|
174
|
+
active_path = home / _SOUL_ACTIVE
|
|
175
|
+
if soul_section.get("active") and (overwrite_soul or not active_path.exists()):
|
|
176
|
+
try:
|
|
177
|
+
active_path.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
active_path.write_text(
|
|
179
|
+
json.dumps(soul_section["active"], indent=2), encoding="utf-8"
|
|
180
|
+
)
|
|
181
|
+
soul_written += 1
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
errors.append(f"soul/active write failed: {exc}")
|
|
184
|
+
|
|
185
|
+
installed_dir = home / _SOUL_INSTALLED_DIR
|
|
186
|
+
for soul_name, soul_data in (soul_section.get("installed") or {}).items():
|
|
187
|
+
soul_file = installed_dir / f"{soul_name}.json"
|
|
188
|
+
if overwrite_soul or not soul_file.exists():
|
|
189
|
+
try:
|
|
190
|
+
soul_file.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
soul_file.write_text(json.dumps(soul_data, indent=2), encoding="utf-8")
|
|
192
|
+
soul_written += 1
|
|
193
|
+
except OSError as exc:
|
|
194
|
+
errors.append(f"soul/installed/{soul_name} write failed: {exc}")
|
|
195
|
+
|
|
196
|
+
summary["soul_files_written"] = soul_written
|
|
197
|
+
|
|
198
|
+
# --- memories ---
|
|
199
|
+
imported_memories = _import_memories(home, bundle.get("memories") or [])
|
|
200
|
+
summary["memories_imported"] = imported_memories
|
|
201
|
+
if imported_memories:
|
|
202
|
+
errors_from_mem: list[str] = [] # import_memories logs but doesn't surface
|
|
203
|
+
logger.info("Imported %d memories", imported_memories)
|
|
204
|
+
|
|
205
|
+
# --- conversations ---
|
|
206
|
+
imported_convs = _import_conversations(
|
|
207
|
+
home, bundle.get("conversations") or {}, overwrite=overwrite_soul
|
|
208
|
+
)
|
|
209
|
+
summary["conversations_imported"] = imported_convs
|
|
210
|
+
|
|
211
|
+
logger.info(
|
|
212
|
+
"Import complete: %d memories, %d conversations, %d soul files",
|
|
213
|
+
summary["memories_imported"],
|
|
214
|
+
summary["conversations_imported"],
|
|
215
|
+
summary["soul_files_written"],
|
|
216
|
+
)
|
|
217
|
+
return summary
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Private helpers — export
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _read_agent_name(home: Path) -> str:
|
|
226
|
+
"""Read the agent name from identity.json or config.yaml."""
|
|
227
|
+
for rel in (_IDENTITY_FILE, _CONFIG_FILE):
|
|
228
|
+
p = home / rel
|
|
229
|
+
if p.exists():
|
|
230
|
+
try:
|
|
231
|
+
if p.suffix in (".yaml", ".yml"):
|
|
232
|
+
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
233
|
+
else:
|
|
234
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
235
|
+
name = data.get("name") or data.get("agent_name")
|
|
236
|
+
if name:
|
|
237
|
+
return str(name)
|
|
238
|
+
except Exception:
|
|
239
|
+
continue
|
|
240
|
+
return "unknown"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _load_identity(home: Path) -> dict:
|
|
244
|
+
p = home / _IDENTITY_FILE
|
|
245
|
+
if not p.exists():
|
|
246
|
+
return {}
|
|
247
|
+
try:
|
|
248
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
logger.warning("Cannot read identity: %s", exc)
|
|
251
|
+
return {}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _load_config(home: Path) -> dict:
|
|
255
|
+
p = home / _CONFIG_FILE
|
|
256
|
+
if not p.exists():
|
|
257
|
+
return {}
|
|
258
|
+
try:
|
|
259
|
+
data = yaml.safe_load(p.read_text(encoding="utf-8"))
|
|
260
|
+
return data if isinstance(data, dict) else {}
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
logger.warning("Cannot read config: %s", exc)
|
|
263
|
+
return {}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _load_soul(home: Path) -> dict:
|
|
267
|
+
soul: dict[str, Any] = {"base": {}, "active": None, "installed": {}}
|
|
268
|
+
|
|
269
|
+
base_p = home / _SOUL_BASE
|
|
270
|
+
if base_p.exists():
|
|
271
|
+
try:
|
|
272
|
+
soul["base"] = json.loads(base_p.read_text(encoding="utf-8"))
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
logger.warning("Cannot read soul/base: %s", exc)
|
|
275
|
+
|
|
276
|
+
active_p = home / _SOUL_ACTIVE
|
|
277
|
+
if active_p.exists():
|
|
278
|
+
try:
|
|
279
|
+
soul["active"] = json.loads(active_p.read_text(encoding="utf-8"))
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
logger.warning("Cannot read soul/active: %s", exc)
|
|
282
|
+
|
|
283
|
+
installed_dir = home / _SOUL_INSTALLED_DIR
|
|
284
|
+
if installed_dir.is_dir():
|
|
285
|
+
for f in sorted(installed_dir.glob("*.json")):
|
|
286
|
+
try:
|
|
287
|
+
soul["installed"][f.stem] = json.loads(f.read_text(encoding="utf-8"))
|
|
288
|
+
except Exception as exc:
|
|
289
|
+
logger.warning("Cannot read soul/installed/%s: %s", f.name, exc)
|
|
290
|
+
|
|
291
|
+
return soul
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _load_memories(home: Path) -> list[dict]:
|
|
295
|
+
"""Load all memories from all layers."""
|
|
296
|
+
entries = list_memories(home, limit=10000)
|
|
297
|
+
result = []
|
|
298
|
+
for e in entries:
|
|
299
|
+
d = e.model_dump(mode="json")
|
|
300
|
+
# Ensure datetimes are ISO strings
|
|
301
|
+
for key in ("created_at", "accessed_at"):
|
|
302
|
+
val = d.get(key)
|
|
303
|
+
if val is not None and hasattr(val, "isoformat"):
|
|
304
|
+
d[key] = val.isoformat()
|
|
305
|
+
result.append(d)
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _load_conversations(home: Path) -> dict[str, list[dict]]:
|
|
310
|
+
"""Load all conversation histories from conversations/ dir."""
|
|
311
|
+
conv_dir = home / _CONVERSATIONS_DIR
|
|
312
|
+
if not conv_dir.is_dir():
|
|
313
|
+
return {}
|
|
314
|
+
|
|
315
|
+
conversations: dict[str, list[dict]] = {}
|
|
316
|
+
for f in sorted(conv_dir.glob("*.json")):
|
|
317
|
+
peer = f.stem
|
|
318
|
+
try:
|
|
319
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
320
|
+
if isinstance(data, list):
|
|
321
|
+
conversations[peer] = data
|
|
322
|
+
elif isinstance(data, dict) and "messages" in data:
|
|
323
|
+
conversations[peer] = data["messages"]
|
|
324
|
+
else:
|
|
325
|
+
conversations[peer] = []
|
|
326
|
+
except Exception as exc:
|
|
327
|
+
logger.warning("Cannot read conversation %s: %s", f.name, exc)
|
|
328
|
+
|
|
329
|
+
return conversations
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
# Private helpers — import
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _validate_bundle(bundle: dict[str, Any]) -> None:
|
|
338
|
+
"""Raise ValueError if the bundle is structurally invalid."""
|
|
339
|
+
if not isinstance(bundle, dict):
|
|
340
|
+
raise ValueError("Bundle must be a JSON object")
|
|
341
|
+
version = bundle.get("bundle_version")
|
|
342
|
+
if version is None:
|
|
343
|
+
raise ValueError("Missing bundle_version field")
|
|
344
|
+
if version != BUNDLE_VERSION:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Unsupported bundle_version {version!r} (expected {BUNDLE_VERSION})"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _import_memories(home: Path, memory_list: list[dict]) -> int:
|
|
351
|
+
"""Import memories from a bundle, preserving original IDs for idempotency.
|
|
352
|
+
|
|
353
|
+
Writes memory JSON files directly (bypassing store()) so the original
|
|
354
|
+
memory_id is preserved. This makes re-importing the same bundle a no-op.
|
|
355
|
+
"""
|
|
356
|
+
if not memory_list:
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
from .models import MemoryEntry
|
|
360
|
+
from .memory_engine import _memory_dir, _update_index
|
|
361
|
+
|
|
362
|
+
mem_dir = _memory_dir(home) # creates layer subdirs
|
|
363
|
+
|
|
364
|
+
# Build set of existing memory IDs from disk
|
|
365
|
+
existing: set[str] = set()
|
|
366
|
+
for layer in MemoryLayer:
|
|
367
|
+
layer_dir = mem_dir / layer.value
|
|
368
|
+
if layer_dir.is_dir():
|
|
369
|
+
for f in layer_dir.glob("*.json"):
|
|
370
|
+
existing.add(f.stem)
|
|
371
|
+
|
|
372
|
+
imported = 0
|
|
373
|
+
for mem_data in memory_list:
|
|
374
|
+
mid = mem_data.get("memory_id", "")
|
|
375
|
+
if not mid or mid in existing:
|
|
376
|
+
continue
|
|
377
|
+
try:
|
|
378
|
+
layer_raw = mem_data.get("layer", "short-term")
|
|
379
|
+
layer = MemoryLayer(layer_raw) if isinstance(layer_raw, str) else MemoryLayer.SHORT_TERM
|
|
380
|
+
entry = MemoryEntry(
|
|
381
|
+
memory_id=mid,
|
|
382
|
+
content=mem_data["content"],
|
|
383
|
+
tags=mem_data.get("tags") or [],
|
|
384
|
+
source=mem_data.get("source", "bundle-import"),
|
|
385
|
+
importance=float(mem_data.get("importance", 0.5)),
|
|
386
|
+
layer=layer,
|
|
387
|
+
metadata=mem_data.get("metadata") or {},
|
|
388
|
+
soul_context=mem_data.get("soul_context"),
|
|
389
|
+
)
|
|
390
|
+
# Write with the original memory_id so re-import is idempotent
|
|
391
|
+
path = mem_dir / layer.value / f"{mid}.json"
|
|
392
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
393
|
+
path.write_text(entry.model_dump_json(indent=2), encoding="utf-8")
|
|
394
|
+
_update_index(home, entry)
|
|
395
|
+
existing.add(mid)
|
|
396
|
+
imported += 1
|
|
397
|
+
except (KeyError, ValueError) as exc:
|
|
398
|
+
logger.warning("Skipping invalid memory in bundle: %s", exc)
|
|
399
|
+
|
|
400
|
+
return imported
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _import_conversations(
|
|
404
|
+
home: Path, conversations: dict[str, list[dict]], overwrite: bool = False
|
|
405
|
+
) -> int:
|
|
406
|
+
"""Import conversation histories, merging per peer."""
|
|
407
|
+
if not conversations:
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
conv_dir = home / _CONVERSATIONS_DIR
|
|
411
|
+
conv_dir.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
|
|
413
|
+
imported = 0
|
|
414
|
+
for peer, messages in conversations.items():
|
|
415
|
+
if not peer or not isinstance(messages, list):
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
peer_file = conv_dir / f"{peer}.json"
|
|
419
|
+
existing_messages: list[dict] = []
|
|
420
|
+
|
|
421
|
+
if peer_file.exists() and not overwrite:
|
|
422
|
+
try:
|
|
423
|
+
existing_data = json.loads(peer_file.read_text(encoding="utf-8"))
|
|
424
|
+
if isinstance(existing_data, list):
|
|
425
|
+
existing_messages = existing_data
|
|
426
|
+
except Exception:
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
# Deduplicate by (role, content, timestamp) tuple
|
|
430
|
+
existing_keys = {
|
|
431
|
+
(m.get("role"), m.get("content"), m.get("timestamp"))
|
|
432
|
+
for m in existing_messages
|
|
433
|
+
}
|
|
434
|
+
new_messages = [
|
|
435
|
+
m for m in messages
|
|
436
|
+
if (m.get("role"), m.get("content"), m.get("timestamp")) not in existing_keys
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
merged = existing_messages + new_messages
|
|
440
|
+
if new_messages or overwrite:
|
|
441
|
+
try:
|
|
442
|
+
peer_file.write_text(json.dumps(merged, indent=2), encoding="utf-8")
|
|
443
|
+
imported += len(new_messages)
|
|
444
|
+
except OSError as exc:
|
|
445
|
+
logger.warning("Cannot write conversation %s: %s", peer, exc)
|
|
446
|
+
|
|
447
|
+
return imported
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Fallback event tracker — graceful degradation logging.
|
|
2
|
+
|
|
3
|
+
Records every LLM fallback event to ~/.skcapstone/fallbacks.json so
|
|
4
|
+
operators can diagnose which backends are failing and how often the
|
|
5
|
+
agent is degrading to lower-quality providers.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
FallbackEvent — Pydantic model for a single fallback occurrence
|
|
9
|
+
FallbackTracker — thread-safe writer / reader for fallbacks.json
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from . import AGENT_HOME
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("skcapstone.fallback_tracker")
|
|
26
|
+
|
|
27
|
+
_DEFAULT_PATH = Path(AGENT_HOME).expanduser() / "fallbacks.json"
|
|
28
|
+
_MAX_EVENTS = 1000 # cap file size; rotate oldest when exceeded
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FallbackEvent(BaseModel):
|
|
32
|
+
"""A single LLM fallback occurrence.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
timestamp: ISO-8601 UTC timestamp of the event.
|
|
36
|
+
primary_model: The model that was originally selected.
|
|
37
|
+
primary_backend: The backend provider of the primary model.
|
|
38
|
+
fallback_model: The model actually used (or ``"none"`` if all failed).
|
|
39
|
+
fallback_backend: The backend that served the response.
|
|
40
|
+
reason: Short human-readable description of why the fallback occurred.
|
|
41
|
+
success: Whether the fallback itself produced a usable response.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
45
|
+
primary_model: str
|
|
46
|
+
primary_backend: str
|
|
47
|
+
fallback_model: str
|
|
48
|
+
fallback_backend: str
|
|
49
|
+
reason: str
|
|
50
|
+
success: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FallbackTracker:
|
|
54
|
+
"""Thread-safe store for fallback events.
|
|
55
|
+
|
|
56
|
+
Events are appended to a JSON file (list of objects). The file is
|
|
57
|
+
created on first write. Reads never raise — a missing or corrupt
|
|
58
|
+
file returns an empty list.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: Path to the fallbacks JSON file.
|
|
62
|
+
Defaults to ``~/.skcapstone/fallbacks.json``.
|
|
63
|
+
max_events: Maximum number of events retained (oldest are pruned).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
path: Optional[Path] = None,
|
|
69
|
+
max_events: int = _MAX_EVENTS,
|
|
70
|
+
) -> None:
|
|
71
|
+
self._path = Path(path) if path is not None else _DEFAULT_PATH
|
|
72
|
+
self._max_events = max_events
|
|
73
|
+
self._lock = threading.Lock()
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Public API
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def record(self, event: FallbackEvent) -> None:
|
|
80
|
+
"""Append *event* to the fallback log.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
event: The fallback event to persist.
|
|
84
|
+
"""
|
|
85
|
+
with self._lock:
|
|
86
|
+
events = self._load_raw()
|
|
87
|
+
events.append(event.model_dump())
|
|
88
|
+
if len(events) > self._max_events:
|
|
89
|
+
events = events[-self._max_events :]
|
|
90
|
+
self._save_raw(events)
|
|
91
|
+
logger.debug(
|
|
92
|
+
"Fallback recorded: %s → %s (%s, success=%s)",
|
|
93
|
+
event.primary_backend,
|
|
94
|
+
event.fallback_backend,
|
|
95
|
+
event.reason,
|
|
96
|
+
event.success,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def load_events(self, limit: int = 0) -> list[FallbackEvent]:
|
|
100
|
+
"""Return stored fallback events, newest first.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
limit: If > 0, return only the *limit* most recent events.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of :class:`FallbackEvent` objects.
|
|
107
|
+
"""
|
|
108
|
+
with self._lock:
|
|
109
|
+
raw = self._load_raw()
|
|
110
|
+
|
|
111
|
+
events: list[FallbackEvent] = []
|
|
112
|
+
for item in reversed(raw):
|
|
113
|
+
try:
|
|
114
|
+
events.append(FallbackEvent(**item))
|
|
115
|
+
except Exception: # noqa: BLE001
|
|
116
|
+
continue # skip corrupt entries
|
|
117
|
+
|
|
118
|
+
if limit > 0:
|
|
119
|
+
return events[:limit]
|
|
120
|
+
return events
|
|
121
|
+
|
|
122
|
+
def clear(self) -> int:
|
|
123
|
+
"""Delete all stored fallback events.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Number of events that were cleared.
|
|
127
|
+
"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
raw = self._load_raw()
|
|
130
|
+
count = len(raw)
|
|
131
|
+
self._save_raw([])
|
|
132
|
+
return count
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def path(self) -> Path:
|
|
136
|
+
"""Path to the fallbacks JSON file."""
|
|
137
|
+
return self._path
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Internal helpers
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _load_raw(self) -> list[dict]:
|
|
144
|
+
"""Load raw JSON list from disk without locking."""
|
|
145
|
+
if not self._path.exists():
|
|
146
|
+
return []
|
|
147
|
+
try:
|
|
148
|
+
text = self._path.read_text(encoding="utf-8")
|
|
149
|
+
data = json.loads(text)
|
|
150
|
+
if isinstance(data, list):
|
|
151
|
+
return data
|
|
152
|
+
except (json.JSONDecodeError, OSError):
|
|
153
|
+
logger.warning("fallbacks.json is corrupt or unreadable — resetting")
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def _save_raw(self, events: list[dict]) -> None:
|
|
157
|
+
"""Write raw JSON list to disk without locking."""
|
|
158
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
self._path.write_text(
|
|
160
|
+
json.dumps(events, indent=2, ensure_ascii=False),
|
|
161
|
+
encoding="utf-8",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Module-level singleton — shared across the process
|
|
166
|
+
_tracker: Optional[FallbackTracker] = None
|
|
167
|
+
_tracker_lock = threading.Lock()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_tracker(path: Optional[Path] = None) -> FallbackTracker:
|
|
171
|
+
"""Return the module-level :class:`FallbackTracker` singleton.
|
|
172
|
+
|
|
173
|
+
Creates it on first call. Passing *path* on the first call
|
|
174
|
+
customises the storage location.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
path: Optional override for the fallbacks file path.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The singleton :class:`FallbackTracker`.
|
|
181
|
+
"""
|
|
182
|
+
global _tracker
|
|
183
|
+
with _tracker_lock:
|
|
184
|
+
if _tracker is None:
|
|
185
|
+
_tracker = FallbackTracker(path=path)
|
|
186
|
+
return _tracker
|