@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,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ContextWindowManager — per-sender token tracking and history compression.
|
|
3
|
+
|
|
4
|
+
Tracks cumulative token usage for each sender's conversation history.
|
|
5
|
+
When a sender's history reaches 80% of ``max_context_tokens``, the oldest
|
|
6
|
+
messages are summarised into a single paragraph by the LLM and replaced in
|
|
7
|
+
the ConversationStore, keeping only the most recent ``_KEEP_RECENT``
|
|
8
|
+
messages verbatim.
|
|
9
|
+
|
|
10
|
+
Token counting: uses ``tiktoken`` (cl100k_base) when installed, otherwise
|
|
11
|
+
falls back to ``len(content) // 4`` (the same char-based estimate used
|
|
12
|
+
throughout the rest of skcapstone).
|
|
13
|
+
|
|
14
|
+
Usage (inside ConsciousnessLoop)::
|
|
15
|
+
|
|
16
|
+
ctx_mgr = ContextWindowManager(home, config.max_context_tokens)
|
|
17
|
+
# After storing a new assistant reply:
|
|
18
|
+
ctx_mgr.check_and_compress(sender, conv_store, bridge)
|
|
19
|
+
# Via MCP tool:
|
|
20
|
+
stats = ctx_mgr.get_all_stats(conv_store)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional, TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from .conversation_store import ConversationStore
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("skcapstone.context_window")
|
|
34
|
+
|
|
35
|
+
# System prompt for the compression LLM call
|
|
36
|
+
_SUMMARIZE_SYSTEM_PROMPT = (
|
|
37
|
+
"You are a concise summarization assistant for a sovereign AI agent framework. "
|
|
38
|
+
"Summarize the following conversation history into exactly one paragraph (3-5 sentences). "
|
|
39
|
+
"Capture: the main topics discussed, any decisions or outcomes reached, and the overall tone. "
|
|
40
|
+
"This summary replaces older messages to free up context window space. "
|
|
41
|
+
"Be factual and direct. Do not use bullet points or headers."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# How many recent messages to keep verbatim (not included in summarization)
|
|
45
|
+
_KEEP_RECENT = 4
|
|
46
|
+
|
|
47
|
+
# Context window fill threshold that triggers compression (80 %)
|
|
48
|
+
_THRESHOLD_PCT = 0.80
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Token helpers (module-level, reusable)
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def count_tokens(text: str) -> int:
|
|
57
|
+
"""Count tokens in *text*.
|
|
58
|
+
|
|
59
|
+
Uses ``tiktoken`` (cl100k_base encoding) when the package is installed.
|
|
60
|
+
Falls back to ``max(1, len(text) // 4)`` (4 chars ≈ 1 token) otherwise.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
text: Input text.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Token count (always >= 1 for non-empty input).
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
import tiktoken # optional dep
|
|
70
|
+
enc = tiktoken.get_encoding("cl100k_base")
|
|
71
|
+
return max(1, len(enc.encode(text)))
|
|
72
|
+
except ImportError:
|
|
73
|
+
return max(1, len(text) // 4)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def count_history_tokens(history: list[dict]) -> int:
|
|
77
|
+
"""Sum token counts for all messages in a history list.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
history: List of message dicts, each expected to have a ``"content"`` key.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Total token estimate across all messages.
|
|
84
|
+
"""
|
|
85
|
+
return sum(count_tokens(str(msg.get("content", ""))) for msg in history)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# ContextWindowManager
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ContextWindowManager:
|
|
94
|
+
"""Tracks per-sender token usage and compresses history at the 80 % threshold.
|
|
95
|
+
|
|
96
|
+
Maintains an in-memory stats table for every peer that has been checked.
|
|
97
|
+
Stats are refreshed on every :meth:`check_and_compress` call and after a
|
|
98
|
+
successful compression.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
home: Agent home directory (used for any future persistence needs).
|
|
102
|
+
max_context_tokens: Model context window token budget. The
|
|
103
|
+
compression threshold is set to 80 % of this value.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, home: Path, max_context_tokens: int = 8000) -> None:
|
|
107
|
+
self._home = Path(home)
|
|
108
|
+
self._max_context_tokens = max_context_tokens
|
|
109
|
+
self._threshold = int(max_context_tokens * _THRESHOLD_PCT)
|
|
110
|
+
# peer -> stats snapshot
|
|
111
|
+
self._stats: dict[str, dict] = {}
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Public API
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def check_and_compress(
|
|
118
|
+
self,
|
|
119
|
+
peer: str,
|
|
120
|
+
store: "ConversationStore",
|
|
121
|
+
bridge=None,
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Check peer history token count; compress if at or over threshold.
|
|
124
|
+
|
|
125
|
+
Loads the full history for *peer* from *store*, counts tokens, and
|
|
126
|
+
updates the in-memory stats table. When the count meets or exceeds
|
|
127
|
+
the 80 % threshold and *bridge* is provided, the oldest messages are
|
|
128
|
+
summarised by the LLM (keeping ``_KEEP_RECENT`` verbatim) and the
|
|
129
|
+
history is atomically replaced on disk via
|
|
130
|
+
:meth:`ConversationStore.replace`.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
peer: Sanitised peer name.
|
|
134
|
+
store: :class:`~skcapstone.conversation_store.ConversationStore`
|
|
135
|
+
instance for reading/writing history.
|
|
136
|
+
bridge: :class:`~skcapstone.consciousness_loop.LLMBridge` used to
|
|
137
|
+
generate the summary. If ``None`` compression is skipped but
|
|
138
|
+
stats are still updated.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
``True`` if the history was compressed, ``False`` otherwise.
|
|
142
|
+
"""
|
|
143
|
+
history = store.load(peer)
|
|
144
|
+
token_count = count_history_tokens(history)
|
|
145
|
+
|
|
146
|
+
self._stats[peer] = {
|
|
147
|
+
"tokens": token_count,
|
|
148
|
+
"messages": len(history),
|
|
149
|
+
"threshold": self._threshold,
|
|
150
|
+
"max_context_tokens": self._max_context_tokens,
|
|
151
|
+
"pct_used": round(token_count / self._max_context_tokens * 100, 1),
|
|
152
|
+
"last_compressed_at": self._stats.get(peer, {}).get("last_compressed_at"),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if token_count < self._threshold:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
if bridge is None:
|
|
159
|
+
logger.warning(
|
|
160
|
+
"Context window at %.1f%% for %s but no bridge — skipping compression",
|
|
161
|
+
self._stats[peer]["pct_used"],
|
|
162
|
+
peer,
|
|
163
|
+
)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
if len(history) <= _KEEP_RECENT:
|
|
167
|
+
logger.debug(
|
|
168
|
+
"Context window at %.1f%% for %s but only %d messages — skipping",
|
|
169
|
+
self._stats[peer]["pct_used"],
|
|
170
|
+
peer,
|
|
171
|
+
len(history),
|
|
172
|
+
)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
to_summarize = history[:-_KEEP_RECENT]
|
|
176
|
+
recent = history[-_KEEP_RECENT:]
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
"Context window %.1f%% for %s — compressing %d older messages",
|
|
180
|
+
self._stats[peer]["pct_used"],
|
|
181
|
+
peer,
|
|
182
|
+
len(to_summarize),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
summary_text = self._call_llm_summarize(peer, to_summarize, bridge)
|
|
186
|
+
if not summary_text:
|
|
187
|
+
logger.warning("LLM summarization returned empty result for %s — skipping", peer)
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
191
|
+
summary_entry: dict = {
|
|
192
|
+
"role": "system",
|
|
193
|
+
"content": (
|
|
194
|
+
f"[Earlier context — {len(to_summarize)} messages summarized]: {summary_text}"
|
|
195
|
+
),
|
|
196
|
+
"timestamp": now,
|
|
197
|
+
"is_summary": True,
|
|
198
|
+
"summarized_count": len(to_summarize),
|
|
199
|
+
}
|
|
200
|
+
new_history = [summary_entry] + recent
|
|
201
|
+
store.replace(peer, new_history)
|
|
202
|
+
|
|
203
|
+
new_token_count = count_history_tokens(new_history)
|
|
204
|
+
self._stats[peer].update(
|
|
205
|
+
{
|
|
206
|
+
"tokens": new_token_count,
|
|
207
|
+
"messages": len(new_history),
|
|
208
|
+
"pct_used": round(new_token_count / self._max_context_tokens * 100, 1),
|
|
209
|
+
"last_compressed_at": now,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
logger.info(
|
|
213
|
+
"Context compressed for %s: %d→%d messages, %d→%d tokens (%.1f%%)",
|
|
214
|
+
peer,
|
|
215
|
+
len(history),
|
|
216
|
+
len(new_history),
|
|
217
|
+
token_count,
|
|
218
|
+
new_token_count,
|
|
219
|
+
self._stats[peer]["pct_used"],
|
|
220
|
+
)
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
def update_stats(self, peer: str, store: "ConversationStore") -> dict:
|
|
224
|
+
"""Refresh and return stats for *peer* without triggering compression.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
peer: Peer name.
|
|
228
|
+
store: :class:`~skcapstone.conversation_store.ConversationStore`.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Stats dict for this peer.
|
|
232
|
+
"""
|
|
233
|
+
history = store.load(peer)
|
|
234
|
+
token_count = count_history_tokens(history)
|
|
235
|
+
self._stats[peer] = {
|
|
236
|
+
"tokens": token_count,
|
|
237
|
+
"messages": len(history),
|
|
238
|
+
"threshold": self._threshold,
|
|
239
|
+
"max_context_tokens": self._max_context_tokens,
|
|
240
|
+
"pct_used": round(token_count / self._max_context_tokens * 100, 1),
|
|
241
|
+
"last_compressed_at": self._stats.get(peer, {}).get("last_compressed_at"),
|
|
242
|
+
}
|
|
243
|
+
return self._stats[peer]
|
|
244
|
+
|
|
245
|
+
def get_all_stats(
|
|
246
|
+
self, store: Optional["ConversationStore"] = None
|
|
247
|
+
) -> dict[str, dict]:
|
|
248
|
+
"""Return current stats for all tracked senders.
|
|
249
|
+
|
|
250
|
+
When *store* is provided any peers that have on-disk history but are
|
|
251
|
+
not yet in the in-memory stats table (e.g. written by a previous
|
|
252
|
+
process) are lazily loaded and included.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
store: Optional :class:`~skcapstone.conversation_store.ConversationStore`
|
|
256
|
+
used to discover and load previously unseen peers.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Mapping of peer name → stats dict.
|
|
260
|
+
"""
|
|
261
|
+
if store is not None:
|
|
262
|
+
for peer in store.all_peers():
|
|
263
|
+
if peer not in self._stats:
|
|
264
|
+
self.update_stats(peer, store)
|
|
265
|
+
return dict(self._stats)
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# Private helpers
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def _call_llm_summarize(
|
|
272
|
+
self, peer: str, messages: list[dict], bridge
|
|
273
|
+
) -> str:
|
|
274
|
+
"""Call *bridge* to produce a one-paragraph summary of *messages*.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
peer: Peer name (included in the summarisation prompt for context).
|
|
278
|
+
messages: Older messages to summarise.
|
|
279
|
+
bridge: :class:`~skcapstone.consciousness_loop.LLMBridge`.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Summary string, or empty string on any failure.
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
from .model_router import TaskSignal
|
|
286
|
+
|
|
287
|
+
lines = [f"Conversation with {peer} ({len(messages)} messages to summarize):"]
|
|
288
|
+
lines.append("")
|
|
289
|
+
for msg in messages:
|
|
290
|
+
role = msg.get("role", "unknown")
|
|
291
|
+
content = str(msg.get("content", "")).strip()
|
|
292
|
+
# Skip existing summary sentinels (nested compression guard)
|
|
293
|
+
if msg.get("is_summary"):
|
|
294
|
+
lines.append(f"[Previous summary]: {content}")
|
|
295
|
+
continue
|
|
296
|
+
label = "Agent" if role == "assistant" else peer.capitalize()
|
|
297
|
+
lines.append(f"{label}: {content}")
|
|
298
|
+
lines.append("")
|
|
299
|
+
lines.append(
|
|
300
|
+
"Summarize the above into one paragraph (3-5 sentences). "
|
|
301
|
+
"Preserve key topics, decisions, and tone."
|
|
302
|
+
)
|
|
303
|
+
prompt_text = "\n".join(lines)
|
|
304
|
+
|
|
305
|
+
signal = TaskSignal(
|
|
306
|
+
description="Compress conversation context window",
|
|
307
|
+
tags=["summary", "context"],
|
|
308
|
+
estimated_tokens=count_tokens(prompt_text),
|
|
309
|
+
)
|
|
310
|
+
result = bridge.generate(_SUMMARIZE_SYSTEM_PROMPT, prompt_text, signal)
|
|
311
|
+
return result or ""
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
logger.warning("Context compression LLM call failed for %s: %s", peer, exc)
|
|
314
|
+
return ""
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConversationManager — centralized manager for all peer conversation histories.
|
|
3
|
+
|
|
4
|
+
Owns the {home}/conversations/ directory. Provides a clean API for adding,
|
|
5
|
+
retrieving, searching, and exporting conversations instead of ad-hoc file
|
|
6
|
+
writes scattered across the codebase.
|
|
7
|
+
|
|
8
|
+
Used by ConsciousnessLoop (via SystemPromptBuilder) and daemon API endpoints.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("skcapstone.conversation_manager")
|
|
22
|
+
|
|
23
|
+
# Allowlist for peer name characters (alphanumeric + safe punctuation)
|
|
24
|
+
_PEER_NAME_SAFE_RE = re.compile(r"[^a-zA-Z0-9_\-@\.]")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sanitize_peer_name(peer: str) -> str:
|
|
28
|
+
"""Sanitize a peer name for safe use as a filesystem key.
|
|
29
|
+
|
|
30
|
+
Strips path separators (/ \\), null bytes, and any character not in the
|
|
31
|
+
alphanumeric + ``-_@.`` set. Caps length at 64 characters. Returns
|
|
32
|
+
``"unknown"`` if the result would be empty.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
peer: Raw peer name.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Filesystem-safe peer name, at most 64 characters long.
|
|
39
|
+
"""
|
|
40
|
+
if not peer or not isinstance(peer, str):
|
|
41
|
+
return "unknown"
|
|
42
|
+
sanitized = peer.replace("\x00", "").replace("/", "").replace("\\", "")
|
|
43
|
+
sanitized = _PEER_NAME_SAFE_RE.sub("", sanitized)
|
|
44
|
+
sanitized = sanitized.strip(".")
|
|
45
|
+
return sanitized[:64] or "unknown"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConversationManager:
|
|
49
|
+
"""Centralized manager for all peer conversation histories.
|
|
50
|
+
|
|
51
|
+
Stores conversations as JSON files under {home}/conversations/{peer}.json.
|
|
52
|
+
Provides atomic writes, in-memory caching, search, and export.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
home: Agent home directory (~/.skcapstone).
|
|
56
|
+
max_history_messages: Maximum messages to retain per peer in memory
|
|
57
|
+
and on disk.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, home: Path, max_history_messages: int = 10) -> None:
|
|
61
|
+
self._home = Path(home)
|
|
62
|
+
self._conversations_dir = self._home / "conversations"
|
|
63
|
+
self._max_history_messages = max_history_messages
|
|
64
|
+
self._history: dict[str, list[dict]] = defaultdict(list)
|
|
65
|
+
self._load_all()
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Public API
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def list_peers(self) -> list[dict]:
|
|
72
|
+
"""List all peers that have conversation history.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of summary dicts, each with keys:
|
|
76
|
+
``peer``, ``message_count``, ``last_message_time``,
|
|
77
|
+
``last_message_preview``. Sorted most-recent-first.
|
|
78
|
+
"""
|
|
79
|
+
peers = []
|
|
80
|
+
for peer, messages in self._history.items():
|
|
81
|
+
if not messages:
|
|
82
|
+
continue
|
|
83
|
+
last = messages[-1]
|
|
84
|
+
peers.append({
|
|
85
|
+
"peer": peer,
|
|
86
|
+
"message_count": len(messages),
|
|
87
|
+
"last_message_time": last.get("timestamp"),
|
|
88
|
+
"last_message_preview": last.get("content", "")[:80],
|
|
89
|
+
})
|
|
90
|
+
peers.sort(key=lambda p: p["last_message_time"] or "", reverse=True)
|
|
91
|
+
return peers
|
|
92
|
+
|
|
93
|
+
def get_history(self, peer: str) -> list[dict]:
|
|
94
|
+
"""Get full conversation history for a peer.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
peer: Peer agent name.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of message dicts with ``role``, ``content``, ``timestamp``.
|
|
101
|
+
Returns an empty list if the peer is unknown.
|
|
102
|
+
"""
|
|
103
|
+
peer = _sanitize_peer_name(peer)
|
|
104
|
+
return list(self._history.get(peer, []))
|
|
105
|
+
|
|
106
|
+
def add_message(self, peer: str, role: str, content: str) -> dict:
|
|
107
|
+
"""Add a message to the peer's conversation history.
|
|
108
|
+
|
|
109
|
+
Appends to in-memory history, caps at ``max_history_messages``, and
|
|
110
|
+
atomically persists to disk.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
peer: Peer agent name.
|
|
114
|
+
role: ``"user"`` or ``"assistant"``.
|
|
115
|
+
content: Message content.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The message dict that was stored (includes ``timestamp``).
|
|
119
|
+
"""
|
|
120
|
+
peer = _sanitize_peer_name(peer)
|
|
121
|
+
msg: dict = {
|
|
122
|
+
"role": role,
|
|
123
|
+
"content": content,
|
|
124
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
125
|
+
}
|
|
126
|
+
self._history[peer].append(msg)
|
|
127
|
+
if len(self._history[peer]) > self._max_history_messages:
|
|
128
|
+
self._history[peer] = self._history[peer][-self._max_history_messages:]
|
|
129
|
+
self._persist(peer)
|
|
130
|
+
return msg
|
|
131
|
+
|
|
132
|
+
def search(self, query: str) -> list[dict]:
|
|
133
|
+
"""Search for a query string across all conversation histories.
|
|
134
|
+
|
|
135
|
+
Case-insensitive substring match against message content.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
query: Search string.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of match dicts, each with ``peer``, ``role``, ``content``,
|
|
142
|
+
``timestamp``.
|
|
143
|
+
"""
|
|
144
|
+
query_lower = query.lower()
|
|
145
|
+
matches: list[dict] = []
|
|
146
|
+
for peer, messages in self._history.items():
|
|
147
|
+
for msg in messages:
|
|
148
|
+
if query_lower in msg.get("content", "").lower():
|
|
149
|
+
matches.append({
|
|
150
|
+
"peer": peer,
|
|
151
|
+
"role": msg.get("role"),
|
|
152
|
+
"content": msg.get("content"),
|
|
153
|
+
"timestamp": msg.get("timestamp"),
|
|
154
|
+
})
|
|
155
|
+
return matches
|
|
156
|
+
|
|
157
|
+
def export_all(self) -> dict[str, list[dict]]:
|
|
158
|
+
"""Export all conversations as a plain dict.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dict mapping peer name → list of message dicts.
|
|
162
|
+
Peers with no messages are excluded.
|
|
163
|
+
"""
|
|
164
|
+
return {peer: list(msgs) for peer, msgs in self._history.items() if msgs}
|
|
165
|
+
|
|
166
|
+
def delete(self, peer: str) -> bool:
|
|
167
|
+
"""Delete a peer's conversation history from memory and disk.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
peer: Peer agent name.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
``True`` if the conversation existed and was deleted.
|
|
174
|
+
"""
|
|
175
|
+
peer = _sanitize_peer_name(peer)
|
|
176
|
+
existed = bool(self._history.pop(peer, None))
|
|
177
|
+
target = self._conversations_dir / f"{peer}.json"
|
|
178
|
+
if target.exists():
|
|
179
|
+
target.unlink()
|
|
180
|
+
return True
|
|
181
|
+
return existed
|
|
182
|
+
|
|
183
|
+
def format_history_for_prompt(self, peer: str, max_messages: int = 10) -> str:
|
|
184
|
+
"""Format recent conversation history for inclusion in a system prompt.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
peer: Peer agent name.
|
|
188
|
+
max_messages: Maximum messages to include.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Formatted history string, or empty string if no history.
|
|
192
|
+
"""
|
|
193
|
+
history = self._history.get(peer, [])
|
|
194
|
+
if not history:
|
|
195
|
+
return ""
|
|
196
|
+
recent = history[-max_messages:]
|
|
197
|
+
lines = [f"Recent conversation with {peer}:"]
|
|
198
|
+
for msg in recent:
|
|
199
|
+
role = msg.get("role", "?")
|
|
200
|
+
content = msg.get("content", "")[:200]
|
|
201
|
+
lines.append(f" [{role}] {content}")
|
|
202
|
+
return "\n".join(lines)
|
|
203
|
+
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
# Private helpers
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _load_all(self) -> None:
|
|
209
|
+
"""Load all peer conversation files from the conversations directory."""
|
|
210
|
+
if not self._conversations_dir.exists():
|
|
211
|
+
return
|
|
212
|
+
for conv_file in self._conversations_dir.glob("*.json"):
|
|
213
|
+
peer = conv_file.stem
|
|
214
|
+
try:
|
|
215
|
+
data = json.loads(conv_file.read_text(encoding="utf-8"))
|
|
216
|
+
if isinstance(data, list):
|
|
217
|
+
self._history[peer] = data[-self._max_history_messages:]
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
logger.debug("Failed to load conversation %s: %s", conv_file, exc)
|
|
220
|
+
|
|
221
|
+
def _persist(self, peer: str) -> None:
|
|
222
|
+
"""Atomically write peer history to {home}/conversations/{peer}.json.
|
|
223
|
+
|
|
224
|
+
Uses a temp file + rename for atomic update, preventing corruption if
|
|
225
|
+
the process is interrupted mid-write.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
peer: Peer agent name (already sanitized).
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
self._conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
target = self._conversations_dir / f"{peer}.json"
|
|
233
|
+
tmp = target.with_suffix(".json.tmp")
|
|
234
|
+
payload = json.dumps(self._history[peer], ensure_ascii=False, indent=2)
|
|
235
|
+
tmp.write_text(payload, encoding="utf-8")
|
|
236
|
+
tmp.replace(target)
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
logger.debug("Failed to persist conversation for %s: %s", peer, exc)
|