novacode-cli 1.2.0__py3-none-any.whl
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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Lessons every installation starts with.
|
|
2
|
+
|
|
3
|
+
Running the learning loop across a dozen unrelated tasks, the only things that
|
|
4
|
+
earned a place in the shared pool were environment facts that are true for
|
|
5
|
+
everyone: run ``apt-get update`` first, how to get Python into a bare container.
|
|
6
|
+
There is no reason for each user to pay to rediscover those, so they ship here.
|
|
7
|
+
|
|
8
|
+
They are recalled exactly like learned lessons — only when what is on screen
|
|
9
|
+
matches — and are never injected wholesale. Each is written symptom-first,
|
|
10
|
+
because the symptom is what a session is looking at when it needs the lesson.
|
|
11
|
+
|
|
12
|
+
Every entry below was hit by an agent on Terminal-Bench and resolved the way it
|
|
13
|
+
says. Keep it that way: add something only once it has been seen to work.
|
|
14
|
+
|
|
15
|
+
This is a Python module rather than a data file because the package ships only
|
|
16
|
+
``*.jinja`` as package data.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
TOPIC = "builtin-environment"
|
|
24
|
+
|
|
25
|
+
LESSONS: tuple[str, ...] = (
|
|
26
|
+
"- **Symptom:** `E: Unable to locate package <name>` from `apt-get install` in a fresh "
|
|
27
|
+
"container. **Cause:** the image ships with no package lists. **Fix:** run `apt-get update` "
|
|
28
|
+
"first, then install.",
|
|
29
|
+
"- **Symptom:** `apt-get install` stops at a `Geographic area:` / `Configuring tzdata` menu, or "
|
|
30
|
+
"hangs with no output. **Cause:** debconf is waiting for an answer and there is no terminal. "
|
|
31
|
+
"**Fix:** `DEBIAN_FRONTEND=noninteractive apt-get install -y <packages>`.",
|
|
32
|
+
"- **Symptom:** `bash: python3: command not found` or `python: command not found`. **Cause:** "
|
|
33
|
+
"the image has no Python (or only `python3`, not `python`). **Fix:** `apt-get update && "
|
|
34
|
+
"apt-get install -y python3 python3-pip`, and call it as `python3`.",
|
|
35
|
+
"- **Symptom:** `error: externally-managed-environment` from `pip install`. **Cause:** PEP 668 — "
|
|
36
|
+
"the system Python refuses global installs. **Fix:** `pip install --break-system-packages "
|
|
37
|
+
"<pkg>` in a throwaway container, or create a venv with `python3 -m venv`.",
|
|
38
|
+
"- **Symptom:** `E: Could not get lock /var/lib/dpkg/lock` or `lock-frontend`. **Cause:** "
|
|
39
|
+
"another apt/dpkg process is still running. **Fix:** wait for it to finish (`while pgrep -x "
|
|
40
|
+
"apt-get >/dev/null || pgrep -x dpkg >/dev/null; do sleep 2; done`); do not delete the lock.",
|
|
41
|
+
"- **Symptom:** `Could not find a suitable TLS CA certificate bundle` or SSL certificate "
|
|
42
|
+
"errors from pip/curl in a minimal image. **Cause:** no CA certificates installed. **Fix:** "
|
|
43
|
+
"`apt-get install -y ca-certificates`.",
|
|
44
|
+
"- **Symptom:** `curl: command not found` (or `timeout: failed to run command 'curl'`). "
|
|
45
|
+
"**Cause:** minimal image. **Fix:** `apt-get install -y curl`, or use `wget` or Python's "
|
|
46
|
+
"`urllib` if one of those is present.",
|
|
47
|
+
"- **Symptom:** a command takes minutes and its output comes back truncated. **Cause:** it "
|
|
48
|
+
"printed megabytes (a binary dump, a runaway loop, a huge log). **Fix:** bound the output — "
|
|
49
|
+
"`| head -c 2000`, `| tail -n 40`, or compare hashes/sizes instead of printing content.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def enabled() -> bool:
|
|
54
|
+
"""Off with ``NOVA_BUILTIN_LESSONS=0``."""
|
|
55
|
+
return os.environ.get("NOVA_BUILTIN_LESSONS", "").strip().lower() not in {"0", "false", "off"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def body() -> str:
|
|
59
|
+
"""The lessons as a topic-file body."""
|
|
60
|
+
return "# Environment basics\n\n" + "\n".join(LESSONS) + "\n"
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""When Nova may dream: the three gates in front of automatic memory consolidation.
|
|
2
|
+
|
|
3
|
+
A dream (``/dream``) consolidates the memory wiki. Run automatically, it must
|
|
4
|
+
neither over-dream (burning tokens on a wiki that barely changed) nor
|
|
5
|
+
under-dream (letting a busy week's lessons sit unconsolidated). It runs only
|
|
6
|
+
when all three gates pass:
|
|
7
|
+
|
|
8
|
+
- **Time gate:** at least :data:`TIME_GATE` since the last dream.
|
|
9
|
+
- **Session gate:** at least :data:`SESSION_GATE` sessions (with messages)
|
|
10
|
+
started since the last dream.
|
|
11
|
+
- **Lock gate:** the consolidation lock is free, so two Nova processes (or a
|
|
12
|
+
manual ``/dream`` and an automatic one) never consolidate at once.
|
|
13
|
+
|
|
14
|
+
A manual ``/dream`` skips the time and session gates (the user asked) but still
|
|
15
|
+
takes the lock, and any finished dream resets both counters.
|
|
16
|
+
|
|
17
|
+
State lives beside the memory it guards: ``<agent_dir>/.dream.json`` and the
|
|
18
|
+
lock file ``<agent_dir>/.dream.lock``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from datetime import UTC, datetime, timedelta
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
#: Minimum time between dreams.
|
|
34
|
+
TIME_GATE = timedelta(hours=24)
|
|
35
|
+
#: Minimum sessions started since the last dream.
|
|
36
|
+
SESSION_GATE = 5
|
|
37
|
+
#: A lock older than this belongs to a dream that died; it may be taken over.
|
|
38
|
+
STALE_LOCK = timedelta(hours=2)
|
|
39
|
+
|
|
40
|
+
_STATE = ".dream.json"
|
|
41
|
+
_LOCK = ".dream.lock"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _now() -> datetime:
|
|
45
|
+
return datetime.now(UTC)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_state(agent_dir: Path) -> dict:
|
|
49
|
+
try:
|
|
50
|
+
return json.loads((agent_dir / _STATE).read_text(encoding="utf-8"))
|
|
51
|
+
except (OSError, ValueError):
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_state(agent_dir: Path, state: dict) -> None:
|
|
56
|
+
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
tmp = agent_dir / f"{_STATE}.tmp"
|
|
58
|
+
tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
59
|
+
tmp.replace(agent_dir / _STATE)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def last_dream(agent_dir: Path) -> datetime:
|
|
63
|
+
"""When the gates were last reset.
|
|
64
|
+
|
|
65
|
+
On the first check there is no record; that moment becomes the baseline, so
|
|
66
|
+
upgrading Nova does not set off a dream over months of old sessions at once.
|
|
67
|
+
"""
|
|
68
|
+
state = _read_state(agent_dir)
|
|
69
|
+
try:
|
|
70
|
+
return datetime.fromisoformat(state["last_dream_at"])
|
|
71
|
+
except (KeyError, TypeError, ValueError):
|
|
72
|
+
now = _now()
|
|
73
|
+
_write_state(agent_dir, {**state, "last_dream_at": now.isoformat(), "trigger": "baseline"})
|
|
74
|
+
return now
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def record_dream(agent_dir: Path, trigger: str, outcome: str) -> None:
|
|
78
|
+
"""Reset the time and session gates after a dream finished."""
|
|
79
|
+
state = _read_state(agent_dir)
|
|
80
|
+
state.update(last_dream_at=_now().isoformat(), trigger=trigger, outcome=outcome)
|
|
81
|
+
_write_state(agent_dir, state)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sessions_since(sessions_dir: Path, since: datetime) -> int:
|
|
85
|
+
"""Sessions with at least one message started after ``since``."""
|
|
86
|
+
cutoff = since.timestamp()
|
|
87
|
+
count = 0
|
|
88
|
+
try:
|
|
89
|
+
entries = list(sessions_dir.iterdir())
|
|
90
|
+
except OSError:
|
|
91
|
+
return 0
|
|
92
|
+
for entry in entries:
|
|
93
|
+
try:
|
|
94
|
+
# A session untouched since the cutoff cannot have started after it;
|
|
95
|
+
# this skips reading most of a large history's meta files.
|
|
96
|
+
if not entry.is_dir() or entry.stat().st_mtime < cutoff:
|
|
97
|
+
continue
|
|
98
|
+
meta = json.loads((entry / "meta.json").read_text(encoding="utf-8"))
|
|
99
|
+
started = datetime.fromisoformat(meta["created_at"])
|
|
100
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
101
|
+
continue
|
|
102
|
+
if started > since and int(meta.get("message_count") or 0) > 0:
|
|
103
|
+
count += 1
|
|
104
|
+
return count
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class GateReport:
|
|
109
|
+
"""Each gate's state, for the decision and for ``/dream status``."""
|
|
110
|
+
|
|
111
|
+
since: datetime
|
|
112
|
+
sessions: int
|
|
113
|
+
lock_holder: str | None # "pid 1234 since 10:02", or None when free
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def time_ok(self) -> bool:
|
|
117
|
+
"""At least :data:`TIME_GATE` since the last dream."""
|
|
118
|
+
return _now() - self.since >= TIME_GATE
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def sessions_ok(self) -> bool:
|
|
122
|
+
"""At least :data:`SESSION_GATE` sessions since the last dream."""
|
|
123
|
+
return self.sessions >= SESSION_GATE
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def lock_ok(self) -> bool:
|
|
127
|
+
"""No other dream holds the lock."""
|
|
128
|
+
return self.lock_holder is None
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def open(self) -> bool:
|
|
132
|
+
"""All three gates pass: an automatic dream may run."""
|
|
133
|
+
return self.time_ok and self.sessions_ok and self.lock_ok
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_gates(agent_dir: Path, sessions_dir: Path) -> GateReport:
|
|
137
|
+
"""Evaluate the three gates (reads files; call off the UI thread)."""
|
|
138
|
+
since = last_dream(agent_dir)
|
|
139
|
+
return GateReport(
|
|
140
|
+
since=since,
|
|
141
|
+
sessions=sessions_since(sessions_dir, since),
|
|
142
|
+
lock_holder=_lock_holder(agent_dir),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _lock_holder(agent_dir: Path) -> str | None:
|
|
147
|
+
"""Who holds a live lock, or None when it is free or stale."""
|
|
148
|
+
try:
|
|
149
|
+
info = json.loads((agent_dir / _LOCK).read_text(encoding="utf-8"))
|
|
150
|
+
taken = datetime.fromisoformat(info["at"])
|
|
151
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
152
|
+
return None
|
|
153
|
+
if _now() - taken > STALE_LOCK:
|
|
154
|
+
return None
|
|
155
|
+
return f"pid {info.get('pid')} since {taken.astimezone().strftime('%H:%M')}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class DreamLock:
|
|
159
|
+
"""The lock gate: an exclusive lock file; ``acquire()`` returns False if held."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, agent_dir: Path) -> None: # noqa: D107
|
|
162
|
+
self._path = agent_dir / _LOCK
|
|
163
|
+
self._held = False
|
|
164
|
+
|
|
165
|
+
def acquire(self) -> bool:
|
|
166
|
+
"""Take the lock, replacing a stale one; False when another dream holds it."""
|
|
167
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
for _ in range(2):
|
|
169
|
+
try:
|
|
170
|
+
fd = os.open(self._path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
171
|
+
except FileExistsError:
|
|
172
|
+
if _lock_holder(self._path.parent) is not None:
|
|
173
|
+
return False
|
|
174
|
+
# Stale (or unreadable): remove and retry once. A racing process
|
|
175
|
+
# that wins the retry keeps the lock; we then report it held.
|
|
176
|
+
try:
|
|
177
|
+
self._path.unlink()
|
|
178
|
+
except OSError:
|
|
179
|
+
return False
|
|
180
|
+
continue
|
|
181
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
182
|
+
json.dump({"pid": os.getpid(), "at": _now().isoformat()}, stream)
|
|
183
|
+
self._held = True
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def release(self) -> None:
|
|
188
|
+
"""Give the lock back (only if this object took it)."""
|
|
189
|
+
if self._held:
|
|
190
|
+
self._held = False
|
|
191
|
+
with contextlib.suppress(OSError):
|
|
192
|
+
self._path.unlink()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared limits for the memory tiers.
|
|
2
|
+
|
|
3
|
+
Single source of truth for the per-file character budget used on both the
|
|
4
|
+
**write** side (Semantic-tier compaction in ``hermes/memory_tiers.py``) and the
|
|
5
|
+
**read** side (prompt-injection truncation in ``memory/agent_memory.py``). These
|
|
6
|
+
were previously two independent ``MAX_MEMORY_CHARS = 12_000`` constants that
|
|
7
|
+
could silently drift.
|
|
8
|
+
|
|
9
|
+
Compaction invariant (keep-newest):
|
|
10
|
+
Memory files are written newest-first (new sections/lessons are prepended,
|
|
11
|
+
above older ones). Both compaction and injection-time truncation therefore
|
|
12
|
+
keep the **head** of the file — which is the most recent content. Any new
|
|
13
|
+
memory writer MUST prepend, not append, to preserve this.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
# Maximum characters per memory file before disk compaction / truncation
|
|
19
|
+
# (~3,000 tokens at 4 chars/token). This is the WRITE-side cap only: it bounds
|
|
20
|
+
# what survives on disk, and is deliberately larger than the injection budget
|
|
21
|
+
# below so raising the budget later can still recover content.
|
|
22
|
+
MAX_MEMORY_CHARS = 12_000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#: Share of the context window ONE injected memory block may occupy. Four
|
|
26
|
+
#: blocks are injected every turn (agent.md, INDEX.md, HABITS.md, project
|
|
27
|
+
#: memory), so the tier as a whole is capped at ~12% of the window.
|
|
28
|
+
MEMORY_WINDOW_FRACTION = 0.03
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#: Default per-block *injection* budget (agent.md, INDEX, HABITS, project
|
|
32
|
+
#: memory). Lowered from the 12k disk cap: the injected head is what the model
|
|
33
|
+
#: reads every turn, and older content is reachable on demand via
|
|
34
|
+
#: ``memory_search``. User-overridable in Nova.config.json
|
|
35
|
+
#: (``memory_block_chars``).
|
|
36
|
+
DEFAULT_MEMORY_BLOCK_CHARS = 6_000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#: Chars of the topic-memory INDEX injected into the system prompt. The index is
|
|
40
|
+
#: a pointer list (newest first); the rest is reachable via ``memory_search``, so
|
|
41
|
+
#: a small slice is enough to orient without paying for ~100k chars of pointers.
|
|
42
|
+
DEFAULT_MEMORY_INDEX_CHARS = 2_000
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def memory_budget(context_window: int, cap: int = DEFAULT_MEMORY_BLOCK_CHARS) -> int:
|
|
46
|
+
"""Chars one injected memory block may use, given the model's window.
|
|
47
|
+
|
|
48
|
+
``MAX_MEMORY_CHARS`` is a *disk* cap and is window-independent — but as an
|
|
49
|
+
injection cap it was the same 12k on a 200K Claude window and on a 40K
|
|
50
|
+
Ollama one, where the four blocks together could claim a third of the
|
|
51
|
+
window before the conversation started. Skills already budget this way
|
|
52
|
+
(``skills.refreshing_middleware.listing_budget``); memory now does too.
|
|
53
|
+
|
|
54
|
+
``cap`` overrides the ceiling (the user-configurable per-block budget);
|
|
55
|
+
the window fraction still applies, so a small window is never overrun.
|
|
56
|
+
"""
|
|
57
|
+
if context_window <= 0:
|
|
58
|
+
return cap
|
|
59
|
+
return max(2_000, min(cap, int(context_window * MEMORY_WINDOW_FRACTION * 4)))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Parsed memory cache with entry and resident-size limits."""
|
|
2
|
+
|
|
3
|
+
# JSON and LangGraph interfaces carry heterogeneous values.
|
|
4
|
+
# ruff: noqa: ANN401
|
|
5
|
+
import hashlib
|
|
6
|
+
import sys
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import Iterator, Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def file_hash(path: Any) -> str:
|
|
14
|
+
"""Hash current contents in bounded chunks regardless of timestamps."""
|
|
15
|
+
digest = hashlib.sha256()
|
|
16
|
+
with path.open("rb") as stream:
|
|
17
|
+
for chunk in iter(lambda: stream.read(64 * 1024), b""):
|
|
18
|
+
digest.update(chunk)
|
|
19
|
+
return digest.hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resident_size(value: Any) -> int:
|
|
23
|
+
"""Count strings and token sets as well as the container holding them."""
|
|
24
|
+
if isinstance(value, (tuple, list, set, frozenset)):
|
|
25
|
+
return sys.getsizeof(value) + sum(resident_size(item) for item in value)
|
|
26
|
+
return sys.getsizeof(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ParsedMemoryCache(OrderedDict):
|
|
30
|
+
"""LRU; conservative accounting counts shared values for each entry."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, max_entries: int = 512, max_bytes: int = 16 * 1024 * 1024) -> None:
|
|
33
|
+
"""Initialize local state without acquiring invocation ownership."""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.max_entries, self.max_bytes = max_entries, max_bytes
|
|
36
|
+
self.resident_bytes = 0
|
|
37
|
+
self._sizes: dict[Any, int] = {}
|
|
38
|
+
|
|
39
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
40
|
+
"""Insert a parsed entry and evict the least recently used entries."""
|
|
41
|
+
size = resident_size(key) + resident_size(value) + 256
|
|
42
|
+
if key in self:
|
|
43
|
+
self.resident_bytes -= self._sizes.pop(key)
|
|
44
|
+
super().__delitem__(key)
|
|
45
|
+
if size > self.max_bytes:
|
|
46
|
+
return
|
|
47
|
+
super().__setitem__(key, value)
|
|
48
|
+
self._sizes[key] = size
|
|
49
|
+
self.resident_bytes += size
|
|
50
|
+
while len(self) > self.max_entries or self.resident_bytes > self.max_bytes:
|
|
51
|
+
oldest, _ = super().popitem(last=False)
|
|
52
|
+
self.resident_bytes -= self._sizes.pop(oldest)
|
|
53
|
+
|
|
54
|
+
def __getitem__(self, key: Any) -> Any:
|
|
55
|
+
"""Read an entry and mark it as most recently used."""
|
|
56
|
+
value = super().__getitem__(key)
|
|
57
|
+
self.move_to_end(key)
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
def __delitem__(self, key: Any) -> None:
|
|
61
|
+
"""Delete a parsed entry and its resident-size accounting."""
|
|
62
|
+
super().__delitem__(key)
|
|
63
|
+
self.resident_bytes -= self._sizes.pop(key)
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
"""Release entries and their resident-size accounting together."""
|
|
67
|
+
super().clear()
|
|
68
|
+
self._sizes.clear()
|
|
69
|
+
self.resident_bytes = 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MemoryCorpus(Mapping):
|
|
73
|
+
"""Keep discovery order while caching parsed bodies independently of corpus size."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, cache: ParsedMemoryCache, tokenize: Any) -> None:
|
|
76
|
+
"""Retain source descriptors; parse individual entries when retrieved."""
|
|
77
|
+
self.cache, self.tokenize = cache, tokenize
|
|
78
|
+
self.sources: dict[str, tuple[Any, Any, str]] = {}
|
|
79
|
+
|
|
80
|
+
def add(self, topic: str, title: Any, source: Path | str, digest: str) -> None:
|
|
81
|
+
"""Register a source without admitting its body into the cache."""
|
|
82
|
+
self.sources[topic] = (title, source, digest)
|
|
83
|
+
|
|
84
|
+
def __len__(self) -> int:
|
|
85
|
+
"""Count discoverable topics, including uncached topics."""
|
|
86
|
+
return len(self.sources)
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Iterator[str]:
|
|
89
|
+
"""Keep the original source ordering."""
|
|
90
|
+
return iter(self.sources)
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, topic: str) -> Any:
|
|
93
|
+
"""Parse on demand and retain only bodies that fit the bounded LRU."""
|
|
94
|
+
title, source, digest = self.sources[topic]
|
|
95
|
+
key = (topic, str(source) if isinstance(source, Path) else "inline", digest)
|
|
96
|
+
if key in self.cache:
|
|
97
|
+
return self.cache[key]
|
|
98
|
+
try:
|
|
99
|
+
body = source.read_text(encoding="utf-8") if isinstance(source, Path) else source
|
|
100
|
+
except OSError:
|
|
101
|
+
# A file can disappear between discovery and retrieval. Do not
|
|
102
|
+
# retain a negative parse; the next discovery sees the deletion.
|
|
103
|
+
return title, "", frozenset()
|
|
104
|
+
value = title, body, self.tokenize(body)
|
|
105
|
+
self.cache[key] = value
|
|
106
|
+
return value
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Which lessons get recalled, and whether recalling them helped.
|
|
2
|
+
|
|
3
|
+
Memory that is only ever written to grows without limit and nobody can tell the
|
|
4
|
+
useful part from the rest. This keeps two counts per lesson:
|
|
5
|
+
|
|
6
|
+
* ``recalls`` — how many times it was put in front of the model;
|
|
7
|
+
* ``resolved`` — how many of those were at a failure that was followed by a
|
|
8
|
+
non-failure. That is a weak signal (the next command may have succeeded for
|
|
9
|
+
any reason), but a lesson recalled twenty times at errors that never clear is
|
|
10
|
+
not earning its place, and one that is never recalled at all is dead weight.
|
|
11
|
+
|
|
12
|
+
Nothing here deletes anything: :func:`unused` and :func:`report` say what the
|
|
13
|
+
evidence is, and a person or a curation pass decides.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_FILE = ".recall-stats.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _key(bullet: str) -> str:
|
|
30
|
+
text = " ".join(bullet.lstrip("-*• ").split()).lower()
|
|
31
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16] # noqa: S324 — an id, not security
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _path(agent_dir: Path) -> Path:
|
|
35
|
+
return agent_dir / "memories" / _FILE
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read(agent_dir: Path) -> dict[str, dict]:
|
|
39
|
+
try:
|
|
40
|
+
data = json.loads(_path(agent_dir).read_text(encoding="utf-8"))
|
|
41
|
+
except (OSError, ValueError):
|
|
42
|
+
return {}
|
|
43
|
+
return data if isinstance(data, dict) else {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _write(agent_dir: Path, stats: dict[str, dict]) -> None:
|
|
47
|
+
try:
|
|
48
|
+
path = _path(agent_dir)
|
|
49
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
path.write_text(json.dumps(stats, indent=1), encoding="utf-8")
|
|
51
|
+
except OSError:
|
|
52
|
+
logger.debug("could not write recall stats", exc_info=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def note_recalled(agent_dir: Path, bullets: list[str]) -> list[str]:
|
|
56
|
+
"""Count one recall for each bullet; returns their keys."""
|
|
57
|
+
if not bullets:
|
|
58
|
+
return []
|
|
59
|
+
stats, now, keys = _read(agent_dir), time.time(), []
|
|
60
|
+
for bullet in bullets:
|
|
61
|
+
key = _key(bullet)
|
|
62
|
+
entry = stats.setdefault(
|
|
63
|
+
key, {"text": bullet.lstrip("-*• ").strip()[:160], "recalls": 0, "resolved": 0, "first": now}
|
|
64
|
+
)
|
|
65
|
+
entry["recalls"] += 1
|
|
66
|
+
entry["last"] = now
|
|
67
|
+
keys.append(key)
|
|
68
|
+
_write(agent_dir, stats)
|
|
69
|
+
return keys
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def note_resolved(agent_dir: Path, keys: list[str]) -> None:
|
|
73
|
+
"""The failure these lessons were recalled at was followed by a non-failure."""
|
|
74
|
+
if not keys:
|
|
75
|
+
return
|
|
76
|
+
stats = _read(agent_dir)
|
|
77
|
+
for key in keys:
|
|
78
|
+
if key in stats:
|
|
79
|
+
stats[key]["resolved"] = stats[key].get("resolved", 0) + 1
|
|
80
|
+
_write(agent_dir, stats)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def report(agent_dir: Path) -> list[dict]:
|
|
84
|
+
"""Every tracked lesson, most recalled first."""
|
|
85
|
+
return sorted(_read(agent_dir).values(), key=lambda e: -e.get("recalls", 0))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def unused(agent_dir: Path, lessons: list[str], *, older_than_days: float = 30.0) -> list[str]:
|
|
89
|
+
"""Lessons that exist but have never been recalled, among ``lessons``.
|
|
90
|
+
|
|
91
|
+
``older_than_days`` cannot be checked for a lesson with no recall record (it
|
|
92
|
+
has no timestamp here), so callers pass only lessons they know to be at
|
|
93
|
+
least that old — typically from the topic file's own review timestamps.
|
|
94
|
+
"""
|
|
95
|
+
del older_than_days # documented contract; ageing is the caller's knowledge
|
|
96
|
+
seen = _read(agent_dir)
|
|
97
|
+
return [bullet for bullet in lessons if _key(bullet) not in seen]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Meaning-level similarity between short texts, computed locally.
|
|
2
|
+
|
|
3
|
+
Lesson handling used to compare words: is this bullet a repeat of that one, has
|
|
4
|
+
another project recorded the same fact, does this lesson bear on the error on
|
|
5
|
+
screen. Word overlap got each of those wrong on real data — it missed four of
|
|
6
|
+
five same-fact pairs, "confirmed" a toolchain note because it shared the phrase
|
|
7
|
+
``apt-get update``, and could not connect ``python3: command not found`` to a
|
|
8
|
+
lesson that only described the fix.
|
|
9
|
+
|
|
10
|
+
This uses the static embedding model Nova already depends on for code search
|
|
11
|
+
(``potion-code-16M`` via ``model2vec``): no network or API call per use, ~1 s to
|
|
12
|
+
load once, and a few hundred texts embed in ~20 ms. If it cannot be loaded every
|
|
13
|
+
caller falls back to word overlap, so nothing depends on it being present.
|
|
14
|
+
|
|
15
|
+
Measured on lessons from Terminal-Bench runs (cosine):
|
|
16
|
+
same fact reworded 0.59-0.81; different facts on one subject 0.34-0.47 (one pair
|
|
17
|
+
0.65); unrelated ~0.0; the error a lesson is about 0.50-0.70; ordinary output
|
|
18
|
+
against every lesson under 0.2.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import threading
|
|
26
|
+
from contextlib import suppress
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from novacode_cli.computation_cache import digest
|
|
30
|
+
from novacode_cli.embeddings import get_model
|
|
31
|
+
from novacode_cli.memory.parsed_cache import ParsedMemoryCache
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
#: Two bullets at or above this say the same thing.
|
|
36
|
+
SAME_FACT = 0.66
|
|
37
|
+
#: A lesson at or above this bears on the text it is compared with.
|
|
38
|
+
RELEVANT = 0.45
|
|
39
|
+
|
|
40
|
+
_CACHE_LIMIT = 4096
|
|
41
|
+
_cache = ParsedMemoryCache(max_entries=_CACHE_LIMIT, max_bytes=8 * 1024 * 1024)
|
|
42
|
+
_cache_model: Any = None
|
|
43
|
+
_embed_lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enabled() -> bool:
|
|
47
|
+
"""False when switched off by ``NOVA_SEMANTIC_MEMORY=0`` or the model is missing."""
|
|
48
|
+
if os.environ.get("NOVA_SEMANTIC_MEMORY", "").strip().lower() in {"0", "false", "off"}:
|
|
49
|
+
return False
|
|
50
|
+
return _load() is not None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load() -> Any: # noqa: ANN401
|
|
54
|
+
return get_model()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def embed(texts: list[str]) -> Any: # noqa: ANN401
|
|
58
|
+
"""Unit-length vectors for ``texts`` (rows), or None when unavailable."""
|
|
59
|
+
if not texts or not enabled():
|
|
60
|
+
return None
|
|
61
|
+
import numpy as np
|
|
62
|
+
|
|
63
|
+
global _cache_model # noqa: PLW0603
|
|
64
|
+
model = _load()
|
|
65
|
+
cache_enabled = os.environ.get("NOVA_DISABLE_COMPUTATION_CACHE") != "1"
|
|
66
|
+
# Concurrent memory searches share one miss computation. The lock also
|
|
67
|
+
# protects LRU promotion and eviction; no network calls happen in encode.
|
|
68
|
+
with _embed_lock:
|
|
69
|
+
if _cache_model is not model:
|
|
70
|
+
_cache.clear()
|
|
71
|
+
_cache_model = model
|
|
72
|
+
unique = dict.fromkeys(texts)
|
|
73
|
+
keys = {text: digest(text) for text in unique}
|
|
74
|
+
found = {}
|
|
75
|
+
for text in unique:
|
|
76
|
+
if cache_enabled:
|
|
77
|
+
with suppress(KeyError):
|
|
78
|
+
found[text] = _cache[keys[text]]
|
|
79
|
+
missing = [text for text in unique if text not in found]
|
|
80
|
+
if missing:
|
|
81
|
+
try:
|
|
82
|
+
vectors = model.encode(missing)
|
|
83
|
+
except Exception: # noqa: BLE001
|
|
84
|
+
logger.debug("embedding failed", exc_info=True)
|
|
85
|
+
return None
|
|
86
|
+
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
|
87
|
+
vectors = vectors / np.where(norms == 0, 1.0, norms)
|
|
88
|
+
for text, vector in zip(missing, vectors, strict=True):
|
|
89
|
+
# A row view would retain the entire batch allocation even if
|
|
90
|
+
# every other row was evicted, defeating the byte budget.
|
|
91
|
+
found[text] = vector.copy()
|
|
92
|
+
if cache_enabled:
|
|
93
|
+
_cache[keys[text]] = found[text]
|
|
94
|
+
return np.stack([found[text] for text in texts])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def best_match(text: str, candidates: list[str]) -> tuple[float, int]:
|
|
98
|
+
"""``(similarity, index)`` of the candidate closest to ``text``; ``(0.0, -1)`` if none."""
|
|
99
|
+
if not text.strip() or not candidates:
|
|
100
|
+
return 0.0, -1
|
|
101
|
+
vectors = embed([text, *candidates])
|
|
102
|
+
if vectors is None:
|
|
103
|
+
return 0.0, -1
|
|
104
|
+
scores = vectors[1:] @ vectors[0]
|
|
105
|
+
index = int(scores.argmax())
|
|
106
|
+
return float(scores[index]), index
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def similarities(text: str, candidates: list[str]) -> list[float]:
|
|
110
|
+
"""Similarity of ``text`` to each candidate; empty when unavailable."""
|
|
111
|
+
if not text.strip() or not candidates:
|
|
112
|
+
return []
|
|
113
|
+
vectors = embed([text, *candidates])
|
|
114
|
+
if vectors is None:
|
|
115
|
+
return []
|
|
116
|
+
return [float(s) for s in vectors[1:] @ vectors[0]]
|