@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,722 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Auto-Promotion Engine — intelligent memory tier management.
|
|
3
|
+
|
|
4
|
+
Periodically sweeps memory layers and promotes qualifying memories based
|
|
5
|
+
on multiple signals: access patterns, importance scores, emotional
|
|
6
|
+
intensity, age, and content relevance.
|
|
7
|
+
|
|
8
|
+
Unlike the curator's simple `should_promote` check, this engine uses a
|
|
9
|
+
weighted scoring system that considers the full context of each memory
|
|
10
|
+
to decide promotion. It also generates summaries for promoted memories
|
|
11
|
+
and tracks promotion history.
|
|
12
|
+
|
|
13
|
+
Architecture:
|
|
14
|
+
The engine scores each memory against promotion criteria:
|
|
15
|
+
- Access frequency (access_count / age_hours)
|
|
16
|
+
- Absolute importance score
|
|
17
|
+
- Emotional intensity (detected from tags/content)
|
|
18
|
+
- Age-based maturity (older important memories promote faster)
|
|
19
|
+
- Tag richness (well-tagged memories are more valuable)
|
|
20
|
+
|
|
21
|
+
Scoring thresholds are configurable per layer transition.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
engine = PromotionEngine(home)
|
|
25
|
+
result = engine.sweep() # Full sweep
|
|
26
|
+
result = engine.sweep(dry_run=True) # Preview only
|
|
27
|
+
result = engine.sweep(layer=MemoryLayer.SHORT_TERM) # Single layer
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import re
|
|
35
|
+
import shutil
|
|
36
|
+
from collections import defaultdict
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from datetime import datetime, timedelta, timezone
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Optional
|
|
41
|
+
|
|
42
|
+
from .memory_engine import (
|
|
43
|
+
_entry_path,
|
|
44
|
+
_load_entry,
|
|
45
|
+
_memory_dir,
|
|
46
|
+
_remove_from_index,
|
|
47
|
+
_save_entry,
|
|
48
|
+
_update_index,
|
|
49
|
+
)
|
|
50
|
+
from .models import MemoryEntry, MemoryLayer
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("skcapstone.memory_promoter")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Scoring configuration
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
# Emotion-related tags that boost promotion scores
|
|
60
|
+
EMOTIONAL_TAGS = frozenset({
|
|
61
|
+
"emotional", "love", "trust", "bond", "cloud9", "feb",
|
|
62
|
+
"breakthrough", "milestone", "joy", "gratitude",
|
|
63
|
+
"connection", "entanglement", "oof", "warmth",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
# High-value content patterns that indicate important memories
|
|
67
|
+
HIGH_VALUE_PATTERNS = [
|
|
68
|
+
re.compile(r"\barchitect", re.I),
|
|
69
|
+
re.compile(r"\bdecision", re.I),
|
|
70
|
+
re.compile(r"\bbreakthrough", re.I),
|
|
71
|
+
re.compile(r"\bmilestone", re.I),
|
|
72
|
+
re.compile(r"\brelease", re.I),
|
|
73
|
+
re.compile(r"\bcritical", re.I),
|
|
74
|
+
re.compile(r"\bsovereign", re.I),
|
|
75
|
+
re.compile(r"\bentangl", re.I),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Tags that protect memories from compression and archival
|
|
79
|
+
PROTECTED_TAGS = frozenset({"seed", "core", "identity"})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class PromotionThresholds:
|
|
84
|
+
"""Configurable thresholds for promotion scoring.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
short_to_mid: Minimum score for short-term to mid-term.
|
|
88
|
+
mid_to_long: Minimum score for mid-term to long-term.
|
|
89
|
+
access_weight: Weight for access frequency signal.
|
|
90
|
+
importance_weight: Weight for importance score.
|
|
91
|
+
emotion_weight: Weight for emotional intensity.
|
|
92
|
+
age_weight: Weight for age-based maturity.
|
|
93
|
+
tag_weight: Weight for tag richness.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
short_to_mid: float = 0.5
|
|
97
|
+
mid_to_long: float = 0.7
|
|
98
|
+
access_weight: float = 0.25
|
|
99
|
+
importance_weight: float = 0.30
|
|
100
|
+
emotion_weight: float = 0.15
|
|
101
|
+
age_weight: float = 0.15
|
|
102
|
+
tag_weight: float = 0.15
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class PromotionCandidate:
|
|
107
|
+
"""A memory evaluated for promotion.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
memory_id: Memory's unique ID.
|
|
111
|
+
current_layer: Current memory tier.
|
|
112
|
+
target_layer: Proposed promotion target.
|
|
113
|
+
score: Computed promotion score (0.0-1.0).
|
|
114
|
+
signals: Breakdown of individual signal scores.
|
|
115
|
+
promoted: Whether promotion was applied.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
memory_id: str
|
|
119
|
+
current_layer: str
|
|
120
|
+
target_layer: str
|
|
121
|
+
score: float
|
|
122
|
+
signals: dict[str, float] = field(default_factory=dict)
|
|
123
|
+
promoted: bool = False
|
|
124
|
+
summary: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class SweepResult:
|
|
129
|
+
"""Results from a promotion sweep.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
scanned: Total memories examined.
|
|
133
|
+
candidates: Memories that scored above threshold.
|
|
134
|
+
promoted: Memories actually promoted.
|
|
135
|
+
skipped: Memories below threshold.
|
|
136
|
+
by_layer: Count per layer after sweep.
|
|
137
|
+
dry_run: Whether this was a preview.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
scanned: int = 0
|
|
141
|
+
candidates: list[PromotionCandidate] = field(default_factory=list)
|
|
142
|
+
promoted: list[PromotionCandidate] = field(default_factory=list)
|
|
143
|
+
skipped: int = 0
|
|
144
|
+
by_layer: dict[str, int] = field(default_factory=dict)
|
|
145
|
+
dry_run: bool = False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# PromotionEngine
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class PromotionEngine:
|
|
154
|
+
"""Intelligent memory promotion engine.
|
|
155
|
+
|
|
156
|
+
Scores memories using multiple signals and promotes qualifying
|
|
157
|
+
ones to higher tiers. Generates summaries and tracks history.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
home: Agent home directory (~/.skcapstone).
|
|
161
|
+
thresholds: Custom scoring thresholds.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
home: Path,
|
|
167
|
+
thresholds: Optional[PromotionThresholds] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self._home = home
|
|
170
|
+
self._thresholds = thresholds or PromotionThresholds()
|
|
171
|
+
|
|
172
|
+
def sweep(
|
|
173
|
+
self,
|
|
174
|
+
layer: Optional[MemoryLayer] = None,
|
|
175
|
+
dry_run: bool = False,
|
|
176
|
+
limit: int = 0,
|
|
177
|
+
) -> SweepResult:
|
|
178
|
+
"""Run a promotion sweep across memory layers.
|
|
179
|
+
|
|
180
|
+
Scans memories, scores them against promotion criteria,
|
|
181
|
+
and promotes qualifying ones. Short-term memories can promote
|
|
182
|
+
to mid-term; mid-term to long-term. Long-term memories are
|
|
183
|
+
never promoted (already at highest tier).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
layer: Restrict sweep to a specific layer. None = all promotable.
|
|
187
|
+
dry_run: Preview promotions without applying them.
|
|
188
|
+
limit: Maximum promotions per sweep (0 = unlimited).
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
SweepResult with details of all evaluations.
|
|
192
|
+
"""
|
|
193
|
+
result = SweepResult(dry_run=dry_run)
|
|
194
|
+
mem_dir = _memory_dir(self._home)
|
|
195
|
+
|
|
196
|
+
layers = [layer] if layer else [MemoryLayer.SHORT_TERM, MemoryLayer.MID_TERM]
|
|
197
|
+
promoted_count = 0
|
|
198
|
+
|
|
199
|
+
for lyr in layers:
|
|
200
|
+
layer_dir = mem_dir / lyr.value
|
|
201
|
+
if not layer_dir.is_dir():
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
for f in sorted(layer_dir.glob("*.json")):
|
|
205
|
+
entry = _load_entry(f)
|
|
206
|
+
if entry is None:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
result.scanned += 1
|
|
210
|
+
candidate = self._evaluate(entry)
|
|
211
|
+
|
|
212
|
+
threshold = self._get_threshold(lyr)
|
|
213
|
+
if candidate.score >= threshold:
|
|
214
|
+
result.candidates.append(candidate)
|
|
215
|
+
|
|
216
|
+
if limit and promoted_count >= limit:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if not dry_run:
|
|
220
|
+
self._promote(entry, f)
|
|
221
|
+
candidate.promoted = True
|
|
222
|
+
candidate.summary = self._generate_summary(entry)
|
|
223
|
+
promoted_count += 1
|
|
224
|
+
|
|
225
|
+
result.promoted.append(candidate)
|
|
226
|
+
else:
|
|
227
|
+
result.skipped += 1
|
|
228
|
+
|
|
229
|
+
# Count layers after sweep
|
|
230
|
+
for lyr in MemoryLayer:
|
|
231
|
+
layer_dir = mem_dir / lyr.value
|
|
232
|
+
if layer_dir.is_dir():
|
|
233
|
+
result.by_layer[lyr.value] = sum(1 for _ in layer_dir.glob("*.json"))
|
|
234
|
+
|
|
235
|
+
# Run maintenance passes: dedup, compress, archive
|
|
236
|
+
if not dry_run:
|
|
237
|
+
try:
|
|
238
|
+
self.dedup_memories()
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.error("Dedup pass failed: %s", exc)
|
|
241
|
+
try:
|
|
242
|
+
self.compress_memories()
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.error("Compress pass failed: %s", exc)
|
|
245
|
+
try:
|
|
246
|
+
self.archive_old_memories()
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
logger.error("Archive pass failed: %s", exc)
|
|
249
|
+
|
|
250
|
+
self._record_sweep(result)
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
def score(self, entry: MemoryEntry) -> PromotionCandidate:
|
|
254
|
+
"""Score a single memory for promotion potential.
|
|
255
|
+
|
|
256
|
+
Useful for inspecting why a particular memory would or
|
|
257
|
+
wouldn't be promoted.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
entry: The MemoryEntry to score.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
PromotionCandidate with score breakdown.
|
|
264
|
+
"""
|
|
265
|
+
return self._evaluate(entry)
|
|
266
|
+
|
|
267
|
+
def get_history(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
268
|
+
"""Read promotion history from the log.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
limit: Maximum entries to return.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of promotion history dicts, newest first.
|
|
275
|
+
"""
|
|
276
|
+
log_path = self._home / "memory" / "promotion-log.json"
|
|
277
|
+
if not log_path.exists():
|
|
278
|
+
return []
|
|
279
|
+
try:
|
|
280
|
+
data = json.loads(log_path.read_text(encoding="utf-8"))
|
|
281
|
+
return data[-limit:]
|
|
282
|
+
except (json.JSONDecodeError, Exception):
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
# -------------------------------------------------------------------
|
|
286
|
+
# Dedup / Compress / Archive
|
|
287
|
+
# -------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def dedup_memories(self) -> int:
|
|
290
|
+
"""Scan all memory tiers for duplicate and near-duplicate memories.
|
|
291
|
+
|
|
292
|
+
Duplicates are detected by:
|
|
293
|
+
- Exact title match (case-insensitive), using the raw JSON ``title``
|
|
294
|
+
field or falling back to the first line of ``content``.
|
|
295
|
+
- Near-duplicate: first 50 characters of the title match.
|
|
296
|
+
|
|
297
|
+
When duplicates are found, the newest memory (by ``created_at``) is
|
|
298
|
+
kept and the rest are moved to an ``archive/deduped/`` directory.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Number of duplicate memories removed.
|
|
302
|
+
"""
|
|
303
|
+
mem_dir = _memory_dir(self._home)
|
|
304
|
+
removed = 0
|
|
305
|
+
|
|
306
|
+
# Collect all memories across tiers with their raw titles
|
|
307
|
+
entries_by_title: dict[str, list[tuple[Path, dict, MemoryEntry]]] = defaultdict(list)
|
|
308
|
+
|
|
309
|
+
for lyr in MemoryLayer:
|
|
310
|
+
layer_dir = mem_dir / lyr.value
|
|
311
|
+
if not layer_dir.is_dir():
|
|
312
|
+
continue
|
|
313
|
+
for f in sorted(layer_dir.glob("*.json")):
|
|
314
|
+
try:
|
|
315
|
+
raw = json.loads(f.read_text(encoding="utf-8"))
|
|
316
|
+
except (json.JSONDecodeError, OSError):
|
|
317
|
+
continue
|
|
318
|
+
entry = _load_entry(f)
|
|
319
|
+
if entry is None:
|
|
320
|
+
continue
|
|
321
|
+
# Use raw title if present, otherwise first line of content
|
|
322
|
+
title = raw.get("title", entry.content.split("\n", 1)[0])
|
|
323
|
+
norm_title = title.strip().lower()
|
|
324
|
+
entries_by_title[norm_title].append((f, raw, entry))
|
|
325
|
+
|
|
326
|
+
# Phase 1: exact title duplicates
|
|
327
|
+
deduped_ids: list[str] = []
|
|
328
|
+
for title, group in entries_by_title.items():
|
|
329
|
+
if len(group) <= 1:
|
|
330
|
+
continue
|
|
331
|
+
# Sort by created_at descending — keep newest
|
|
332
|
+
group.sort(key=lambda g: g[2].created_at, reverse=True)
|
|
333
|
+
keeper = group[0]
|
|
334
|
+
for path, raw, entry in group[1:]:
|
|
335
|
+
self._archive_deduped(path, entry)
|
|
336
|
+
deduped_ids.append(entry.memory_id)
|
|
337
|
+
removed += 1
|
|
338
|
+
logger.info(
|
|
339
|
+
"Dedup: archived %s (dup of %s, title='%s')",
|
|
340
|
+
entry.memory_id, keeper[2].memory_id, title[:60],
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Phase 2: near-duplicates (first 50 chars of title match)
|
|
344
|
+
# Re-collect surviving memories (exclude already-deduped)
|
|
345
|
+
prefix_groups: dict[str, list[tuple[Path, dict, MemoryEntry]]] = defaultdict(list)
|
|
346
|
+
for title, group in entries_by_title.items():
|
|
347
|
+
for path, raw, entry in group:
|
|
348
|
+
if entry.memory_id in deduped_ids:
|
|
349
|
+
continue
|
|
350
|
+
if not path.exists():
|
|
351
|
+
continue
|
|
352
|
+
prefix = title[:50]
|
|
353
|
+
prefix_groups[prefix].append((path, raw, entry))
|
|
354
|
+
|
|
355
|
+
for prefix, group in prefix_groups.items():
|
|
356
|
+
if len(group) <= 1:
|
|
357
|
+
continue
|
|
358
|
+
group.sort(key=lambda g: g[2].created_at, reverse=True)
|
|
359
|
+
keeper = group[0]
|
|
360
|
+
for path, raw, entry in group[1:]:
|
|
361
|
+
if entry.memory_id in deduped_ids:
|
|
362
|
+
continue
|
|
363
|
+
self._archive_deduped(path, entry)
|
|
364
|
+
deduped_ids.append(entry.memory_id)
|
|
365
|
+
removed += 1
|
|
366
|
+
logger.info(
|
|
367
|
+
"Dedup (near): archived %s (near-dup of %s, prefix='%s')",
|
|
368
|
+
entry.memory_id, keeper[2].memory_id, prefix[:50],
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Log dedup actions to promotion-log.json
|
|
372
|
+
if removed > 0:
|
|
373
|
+
self._record_dedup(removed, deduped_ids)
|
|
374
|
+
|
|
375
|
+
return removed
|
|
376
|
+
|
|
377
|
+
def compress_memories(self) -> int:
|
|
378
|
+
"""Compress older memories by truncating content.
|
|
379
|
+
|
|
380
|
+
Rules:
|
|
381
|
+
- Mid-term memories older than 7 days: truncate to first 500 chars + "..."
|
|
382
|
+
- Long-term memories older than 30 days: keep only first 200 chars as summary
|
|
383
|
+
- Memories tagged "seed", "core", or "identity" are never compressed.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Number of memories compressed.
|
|
387
|
+
"""
|
|
388
|
+
mem_dir = _memory_dir(self._home)
|
|
389
|
+
now = datetime.now(timezone.utc)
|
|
390
|
+
compressed = 0
|
|
391
|
+
|
|
392
|
+
compress_rules = [
|
|
393
|
+
(MemoryLayer.MID_TERM, timedelta(days=7), 500),
|
|
394
|
+
(MemoryLayer.LONG_TERM, timedelta(days=30), 200),
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
for lyr, age_threshold, max_chars in compress_rules:
|
|
398
|
+
layer_dir = mem_dir / lyr.value
|
|
399
|
+
if not layer_dir.is_dir():
|
|
400
|
+
continue
|
|
401
|
+
for f in sorted(layer_dir.glob("*.json")):
|
|
402
|
+
entry = _load_entry(f)
|
|
403
|
+
if entry is None:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
# Skip protected memories
|
|
407
|
+
if self._is_protected(entry):
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
age = now - entry.created_at
|
|
411
|
+
if age < age_threshold:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# Already short enough — skip
|
|
415
|
+
if len(entry.content) <= max_chars:
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
entry.content = entry.content[:max_chars] + "..."
|
|
419
|
+
_save_entry(self._home, entry)
|
|
420
|
+
compressed += 1
|
|
421
|
+
logger.info(
|
|
422
|
+
"Compressed %s (%s, age=%dd, truncated to %d chars)",
|
|
423
|
+
entry.memory_id, lyr.value, age.days, max_chars,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return compressed
|
|
427
|
+
|
|
428
|
+
def archive_old_memories(self) -> int:
|
|
429
|
+
"""Move memories older than 60 days to an archive directory.
|
|
430
|
+
|
|
431
|
+
Scans all tiers and moves qualifying memories to
|
|
432
|
+
``~/.skcapstone/agents/<agent>/memory/archive/``.
|
|
433
|
+
Memories tagged "seed", "core", or "identity" are never archived.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Number of memories archived.
|
|
437
|
+
"""
|
|
438
|
+
mem_dir = _memory_dir(self._home)
|
|
439
|
+
archive_dir = mem_dir / "archive"
|
|
440
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
441
|
+
now = datetime.now(timezone.utc)
|
|
442
|
+
threshold = timedelta(days=60)
|
|
443
|
+
archived = 0
|
|
444
|
+
|
|
445
|
+
for lyr in MemoryLayer:
|
|
446
|
+
layer_dir = mem_dir / lyr.value
|
|
447
|
+
if not layer_dir.is_dir():
|
|
448
|
+
continue
|
|
449
|
+
for f in sorted(layer_dir.glob("*.json")):
|
|
450
|
+
entry = _load_entry(f)
|
|
451
|
+
if entry is None:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
if self._is_protected(entry):
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
age = now - entry.created_at
|
|
458
|
+
if age < threshold:
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
dest = archive_dir / f.name
|
|
462
|
+
shutil.move(str(f), str(dest))
|
|
463
|
+
_remove_from_index(self._home, entry.memory_id)
|
|
464
|
+
archived += 1
|
|
465
|
+
logger.info(
|
|
466
|
+
"Archived %s (%s, age=%dd) -> archive/",
|
|
467
|
+
entry.memory_id, lyr.value, age.days,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return archived
|
|
471
|
+
|
|
472
|
+
def _is_protected(self, entry: MemoryEntry) -> bool:
|
|
473
|
+
"""Check if a memory has protected tags (seed/core/identity).
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
entry: The MemoryEntry to check.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
True if the memory should not be compressed or archived.
|
|
480
|
+
"""
|
|
481
|
+
return bool(set(t.lower() for t in entry.tags) & PROTECTED_TAGS)
|
|
482
|
+
|
|
483
|
+
def _archive_deduped(self, path: Path, entry: MemoryEntry) -> None:
|
|
484
|
+
"""Move a deduplicated memory to the deduped archive.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
path: Current file path.
|
|
488
|
+
entry: The MemoryEntry being archived.
|
|
489
|
+
"""
|
|
490
|
+
mem_dir = _memory_dir(self._home)
|
|
491
|
+
dedup_dir = mem_dir / "archive" / "deduped"
|
|
492
|
+
dedup_dir.mkdir(parents=True, exist_ok=True)
|
|
493
|
+
dest = dedup_dir / path.name
|
|
494
|
+
if path.exists():
|
|
495
|
+
shutil.move(str(path), str(dest))
|
|
496
|
+
_remove_from_index(self._home, entry.memory_id)
|
|
497
|
+
|
|
498
|
+
def _record_dedup(self, count: int, deduped_ids: list[str]) -> None:
|
|
499
|
+
"""Append dedup results to the promotion log.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
count: Number of duplicates removed.
|
|
503
|
+
deduped_ids: List of memory IDs that were archived.
|
|
504
|
+
"""
|
|
505
|
+
log_path = self._home / "memory" / "promotion-log.json"
|
|
506
|
+
history: list[dict] = []
|
|
507
|
+
if log_path.exists():
|
|
508
|
+
try:
|
|
509
|
+
history = json.loads(log_path.read_text(encoding="utf-8"))
|
|
510
|
+
except (json.JSONDecodeError, Exception):
|
|
511
|
+
history = []
|
|
512
|
+
|
|
513
|
+
entry = {
|
|
514
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
515
|
+
"action": "dedup",
|
|
516
|
+
"removed": count,
|
|
517
|
+
"deduped_ids": deduped_ids[-50:], # cap list size
|
|
518
|
+
}
|
|
519
|
+
history.append(entry)
|
|
520
|
+
|
|
521
|
+
if len(history) > 100:
|
|
522
|
+
history = history[-100:]
|
|
523
|
+
|
|
524
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
525
|
+
log_path.write_text(json.dumps(history, indent=2), encoding="utf-8")
|
|
526
|
+
|
|
527
|
+
# -------------------------------------------------------------------
|
|
528
|
+
# Scoring
|
|
529
|
+
# -------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
def _evaluate(self, entry: MemoryEntry) -> PromotionCandidate:
|
|
532
|
+
"""Evaluate a memory entry for promotion.
|
|
533
|
+
|
|
534
|
+
Computes a weighted score from multiple signals:
|
|
535
|
+
- Access frequency: access_count normalized by age
|
|
536
|
+
- Importance: raw importance score
|
|
537
|
+
- Emotional intensity: presence of emotional tags
|
|
538
|
+
- Age maturity: older important memories score higher
|
|
539
|
+
- Tag richness: well-tagged memories are more organized
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
entry: The MemoryEntry to evaluate.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
PromotionCandidate with computed score.
|
|
546
|
+
"""
|
|
547
|
+
t = self._thresholds
|
|
548
|
+
|
|
549
|
+
access_score = self._score_access(entry)
|
|
550
|
+
importance_score = entry.importance
|
|
551
|
+
emotion_score = self._score_emotion(entry)
|
|
552
|
+
age_score = self._score_age(entry)
|
|
553
|
+
tag_score = self._score_tags(entry)
|
|
554
|
+
|
|
555
|
+
weighted = (
|
|
556
|
+
access_score * t.access_weight
|
|
557
|
+
+ importance_score * t.importance_weight
|
|
558
|
+
+ emotion_score * t.emotion_weight
|
|
559
|
+
+ age_score * t.age_weight
|
|
560
|
+
+ tag_score * t.tag_weight
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Clamp to [0, 1]
|
|
564
|
+
score = max(0.0, min(1.0, weighted))
|
|
565
|
+
|
|
566
|
+
target = self._target_layer(entry.layer)
|
|
567
|
+
|
|
568
|
+
return PromotionCandidate(
|
|
569
|
+
memory_id=entry.memory_id,
|
|
570
|
+
current_layer=entry.layer.value,
|
|
571
|
+
target_layer=target,
|
|
572
|
+
score=round(score, 4),
|
|
573
|
+
signals={
|
|
574
|
+
"access": round(access_score, 4),
|
|
575
|
+
"importance": round(importance_score, 4),
|
|
576
|
+
"emotion": round(emotion_score, 4),
|
|
577
|
+
"age": round(age_score, 4),
|
|
578
|
+
"tags": round(tag_score, 4),
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def _score_access(self, entry: MemoryEntry) -> float:
|
|
583
|
+
"""Score based on access frequency.
|
|
584
|
+
|
|
585
|
+
Higher access count relative to age = more valuable.
|
|
586
|
+
"""
|
|
587
|
+
age = max(entry.age_hours, 1.0)
|
|
588
|
+
# Normalize: 1 access per hour = score 1.0
|
|
589
|
+
freq = entry.access_count / age
|
|
590
|
+
return min(1.0, freq * 10)
|
|
591
|
+
|
|
592
|
+
def _score_emotion(self, entry: MemoryEntry) -> float:
|
|
593
|
+
"""Score based on emotional content.
|
|
594
|
+
|
|
595
|
+
Checks tags and content for emotional indicators.
|
|
596
|
+
"""
|
|
597
|
+
tag_hits = sum(1 for t in entry.tags if t.lower() in EMOTIONAL_TAGS)
|
|
598
|
+
content_hits = sum(
|
|
599
|
+
1 for p in HIGH_VALUE_PATTERNS if p.search(entry.content)
|
|
600
|
+
)
|
|
601
|
+
# Normalize: 3+ hits = max score
|
|
602
|
+
return min(1.0, (tag_hits + content_hits) / 3)
|
|
603
|
+
|
|
604
|
+
def _score_age(self, entry: MemoryEntry) -> float:
|
|
605
|
+
"""Score based on age-importance interaction.
|
|
606
|
+
|
|
607
|
+
Older memories with high importance score higher — they've
|
|
608
|
+
proven their worth by persisting.
|
|
609
|
+
"""
|
|
610
|
+
age = entry.age_hours
|
|
611
|
+
if entry.layer == MemoryLayer.SHORT_TERM:
|
|
612
|
+
# Short-term: promote after 24h if important
|
|
613
|
+
if age > 24:
|
|
614
|
+
return min(1.0, entry.importance * (age / 72))
|
|
615
|
+
return 0.0
|
|
616
|
+
# Mid-term: promote after 168h (1 week) if important
|
|
617
|
+
if age > 168:
|
|
618
|
+
return min(1.0, entry.importance * (age / 720))
|
|
619
|
+
return 0.0
|
|
620
|
+
|
|
621
|
+
def _score_tags(self, entry: MemoryEntry) -> float:
|
|
622
|
+
"""Score based on tag richness.
|
|
623
|
+
|
|
624
|
+
Well-tagged memories indicate organized, valuable content.
|
|
625
|
+
"""
|
|
626
|
+
n = len(entry.tags)
|
|
627
|
+
if n == 0:
|
|
628
|
+
return 0.0
|
|
629
|
+
# 5+ tags = max score
|
|
630
|
+
return min(1.0, n / 5)
|
|
631
|
+
|
|
632
|
+
# -------------------------------------------------------------------
|
|
633
|
+
# Promotion
|
|
634
|
+
# -------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
def _promote(self, entry: MemoryEntry, old_path: Path) -> None:
|
|
637
|
+
"""Promote a memory to the next tier.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
entry: The MemoryEntry to promote.
|
|
641
|
+
old_path: Current file path (will be removed).
|
|
642
|
+
"""
|
|
643
|
+
old_layer = entry.layer
|
|
644
|
+
|
|
645
|
+
if entry.layer == MemoryLayer.SHORT_TERM:
|
|
646
|
+
entry.layer = MemoryLayer.MID_TERM
|
|
647
|
+
elif entry.layer == MemoryLayer.MID_TERM:
|
|
648
|
+
entry.layer = MemoryLayer.LONG_TERM
|
|
649
|
+
else:
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
if old_path.exists():
|
|
653
|
+
old_path.unlink()
|
|
654
|
+
|
|
655
|
+
_save_entry(self._home, entry)
|
|
656
|
+
_update_index(self._home, entry)
|
|
657
|
+
|
|
658
|
+
logger.info(
|
|
659
|
+
"Promoted %s: %s -> %s",
|
|
660
|
+
entry.memory_id, old_layer.value, entry.layer.value,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def _generate_summary(self, entry: MemoryEntry) -> str:
|
|
664
|
+
"""Generate a short summary for a promoted memory.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
entry: The promoted MemoryEntry.
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
A brief summary string.
|
|
671
|
+
"""
|
|
672
|
+
content = entry.content
|
|
673
|
+
if len(content) <= 80:
|
|
674
|
+
return content
|
|
675
|
+
return content[:77] + "..."
|
|
676
|
+
|
|
677
|
+
def _target_layer(self, current: MemoryLayer) -> str:
|
|
678
|
+
"""Get the promotion target layer name."""
|
|
679
|
+
if current == MemoryLayer.SHORT_TERM:
|
|
680
|
+
return MemoryLayer.MID_TERM.value
|
|
681
|
+
if current == MemoryLayer.MID_TERM:
|
|
682
|
+
return MemoryLayer.LONG_TERM.value
|
|
683
|
+
return current.value
|
|
684
|
+
|
|
685
|
+
def _get_threshold(self, layer: MemoryLayer) -> float:
|
|
686
|
+
"""Get the promotion threshold for a layer."""
|
|
687
|
+
if layer == MemoryLayer.SHORT_TERM:
|
|
688
|
+
return self._thresholds.short_to_mid
|
|
689
|
+
return self._thresholds.mid_to_long
|
|
690
|
+
|
|
691
|
+
# -------------------------------------------------------------------
|
|
692
|
+
# History
|
|
693
|
+
# -------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
def _record_sweep(self, result: SweepResult) -> None:
|
|
696
|
+
"""Append sweep results to the promotion log."""
|
|
697
|
+
log_path = self._home / "memory" / "promotion-log.json"
|
|
698
|
+
history: list[dict] = []
|
|
699
|
+
if log_path.exists():
|
|
700
|
+
try:
|
|
701
|
+
history = json.loads(log_path.read_text(encoding="utf-8"))
|
|
702
|
+
except (json.JSONDecodeError, Exception):
|
|
703
|
+
history = []
|
|
704
|
+
|
|
705
|
+
entry = {
|
|
706
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
707
|
+
"scanned": result.scanned,
|
|
708
|
+
"candidates": len(result.candidates),
|
|
709
|
+
"promoted": len(result.promoted),
|
|
710
|
+
"skipped": result.skipped,
|
|
711
|
+
"dry_run": result.dry_run,
|
|
712
|
+
"by_layer": result.by_layer,
|
|
713
|
+
"promoted_ids": [c.memory_id for c in result.promoted],
|
|
714
|
+
}
|
|
715
|
+
history.append(entry)
|
|
716
|
+
|
|
717
|
+
# Keep last 100 entries
|
|
718
|
+
if len(history) > 100:
|
|
719
|
+
history = history[-100:]
|
|
720
|
+
|
|
721
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
722
|
+
log_path.write_text(json.dumps(history, indent=2), encoding="utf-8")
|