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,248 @@
|
|
|
1
|
+
"""Versioned, checksummed, atomically published full-message recovery archives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# JSON and LangGraph interfaces carry heterogeneous values.
|
|
11
|
+
# ruff: noqa: ANN401
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from langchain_core.messages import messages_from_dict, messages_to_dict
|
|
15
|
+
|
|
16
|
+
from novacode_cli.hermes.activation import content_hash, publish
|
|
17
|
+
|
|
18
|
+
MANIFEST_VERSION = 2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RecoveryArchiveError(ValueError):
|
|
22
|
+
"""An archive is incomplete or its checksum does not match."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecoveryStore:
|
|
26
|
+
"""Keep immutable archives and identify older incomplete transcript files."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, root: Path) -> None:
|
|
29
|
+
"""Initialize local state without acquiring invocation ownership."""
|
|
30
|
+
self.root = Path(root)
|
|
31
|
+
|
|
32
|
+
def _blob(self, data: bytes, blobs: dict) -> Any:
|
|
33
|
+
digest = content_hash(data)
|
|
34
|
+
path = self.root / "blobs" / digest
|
|
35
|
+
if path.exists():
|
|
36
|
+
if path.read_bytes() != data:
|
|
37
|
+
message = "Immutable recovery blob checksum conflict"
|
|
38
|
+
raise RecoveryArchiveError(message)
|
|
39
|
+
else:
|
|
40
|
+
publish(path, data)
|
|
41
|
+
blobs[digest] = len(data)
|
|
42
|
+
return {"__nova_binary__": digest}
|
|
43
|
+
|
|
44
|
+
def _encode(self, value: Any, blobs: Any) -> Any:
|
|
45
|
+
if isinstance(value, bytes):
|
|
46
|
+
return self._blob(value, blobs)
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
if set(value) in ({"__nova_binary__"}, {"__nova_mapping__"}):
|
|
49
|
+
return {
|
|
50
|
+
"__nova_mapping__": [
|
|
51
|
+
[key, self._encode(item, blobs)] for key, item in value.items()
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
return {key: self._encode(item, blobs) for key, item in value.items()}
|
|
55
|
+
if isinstance(value, (tuple, list)):
|
|
56
|
+
return [self._encode(item, blobs) for item in value]
|
|
57
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
58
|
+
return value
|
|
59
|
+
message = f"Unsupported archive value: {type(value).__name__}"
|
|
60
|
+
raise RecoveryArchiveError(message)
|
|
61
|
+
|
|
62
|
+
def _read_blob(self, digest: str) -> bytes:
|
|
63
|
+
if not re.fullmatch(r"[0-9a-f]{64}", digest):
|
|
64
|
+
message = "Invalid blob reference"
|
|
65
|
+
raise RecoveryArchiveError(message)
|
|
66
|
+
data = (self.root / "blobs" / digest).read_bytes()
|
|
67
|
+
if content_hash(data) != digest:
|
|
68
|
+
message = "Recovery blob checksum mismatch"
|
|
69
|
+
raise RecoveryArchiveError(message)
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
def _decode(self, value: Any) -> Any:
|
|
73
|
+
if isinstance(value, dict):
|
|
74
|
+
if set(value) == {"__nova_mapping__"}:
|
|
75
|
+
return {key: self._decode(item) for key, item in value["__nova_mapping__"]}
|
|
76
|
+
if set(value) == {"__nova_binary__"}:
|
|
77
|
+
return self._read_blob(value["__nova_binary__"])
|
|
78
|
+
return {key: self._decode(item) for key, item in value.items()}
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
return [self._decode(item) for item in value]
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
def archive(
|
|
84
|
+
self,
|
|
85
|
+
thread: str,
|
|
86
|
+
messages: list,
|
|
87
|
+
*,
|
|
88
|
+
approvals: Any = (),
|
|
89
|
+
state: Any = None,
|
|
90
|
+
source: str = "manual",
|
|
91
|
+
checkpoint: Any = None,
|
|
92
|
+
) -> Path:
|
|
93
|
+
"""Publish a complete manifest after all message blobs are durable."""
|
|
94
|
+
blobs: dict[str, int] = {}
|
|
95
|
+
payload = self._encode(
|
|
96
|
+
{
|
|
97
|
+
"messages": messages_to_dict(messages),
|
|
98
|
+
"approvals": approvals,
|
|
99
|
+
"state": state or {},
|
|
100
|
+
"checkpoint": checkpoint,
|
|
101
|
+
},
|
|
102
|
+
blobs,
|
|
103
|
+
)
|
|
104
|
+
encoded = json.dumps(payload, ensure_ascii=False, allow_nan=False).encode()
|
|
105
|
+
digest = content_hash(encoded)
|
|
106
|
+
publish(self.root / "blobs" / digest, encoded)
|
|
107
|
+
manifest = {
|
|
108
|
+
"version": MANIFEST_VERSION,
|
|
109
|
+
"id": uuid.uuid4().hex,
|
|
110
|
+
"thread": thread,
|
|
111
|
+
"source": source,
|
|
112
|
+
"message_count": len(messages),
|
|
113
|
+
"payload": digest,
|
|
114
|
+
"blobs": blobs,
|
|
115
|
+
"complete": True,
|
|
116
|
+
}
|
|
117
|
+
manifest["checksum"] = content_hash(json.dumps(manifest, sort_keys=True).encode())
|
|
118
|
+
destination = self.root / "manifests" / f"{manifest['id']}.json"
|
|
119
|
+
publish(destination, json.dumps(manifest, sort_keys=True).encode())
|
|
120
|
+
return destination
|
|
121
|
+
|
|
122
|
+
def inspect(self, reference: str) -> Any:
|
|
123
|
+
"""Validate an archive and all its referenced blobs before returning it."""
|
|
124
|
+
identity = Path(reference).stem
|
|
125
|
+
if not re.fullmatch(r"[0-9a-f]{32}", identity):
|
|
126
|
+
message = "Use a recovery ID from /context recovery"
|
|
127
|
+
raise RecoveryArchiveError(message)
|
|
128
|
+
manifest = json.loads((self.root / "manifests" / f"{identity}.json").read_text())
|
|
129
|
+
if manifest.get("version") not in {1, 2} or not manifest.get("complete"):
|
|
130
|
+
message = "Incomplete or unsupported recovery manifest"
|
|
131
|
+
raise RecoveryArchiveError(message)
|
|
132
|
+
if manifest["version"] == MANIFEST_VERSION:
|
|
133
|
+
checked = dict(manifest)
|
|
134
|
+
checksum = checked.pop("checksum", None)
|
|
135
|
+
if checksum != content_hash(json.dumps(checked, sort_keys=True).encode()):
|
|
136
|
+
message = "Recovery manifest checksum mismatch"
|
|
137
|
+
raise RecoveryArchiveError(message)
|
|
138
|
+
else:
|
|
139
|
+
manifest["integrity"] = "legacy: payload verified; manifest checksum unavailable"
|
|
140
|
+
if not isinstance(manifest.get("blobs"), dict) or not isinstance(
|
|
141
|
+
manifest.get("payload"), str
|
|
142
|
+
):
|
|
143
|
+
message = "Incomplete recovery manifest references"
|
|
144
|
+
raise RecoveryArchiveError(message)
|
|
145
|
+
payload = self._decode(json.loads(self._read_blob(manifest["payload"])))
|
|
146
|
+
for digest, size in manifest["blobs"].items():
|
|
147
|
+
if len(self._read_blob(digest)) != size:
|
|
148
|
+
message = "Recovery blob length mismatch"
|
|
149
|
+
raise RecoveryArchiveError(message)
|
|
150
|
+
return manifest, payload
|
|
151
|
+
|
|
152
|
+
def list(self, thread: str | None = None) -> Any:
|
|
153
|
+
"""List recoverable archives and label damaged references."""
|
|
154
|
+
entries = []
|
|
155
|
+
for path in sorted((self.root / "manifests").glob("*.json")):
|
|
156
|
+
try:
|
|
157
|
+
manifest, _ = self.inspect(path.stem)
|
|
158
|
+
if thread is None or manifest["thread"] == thread:
|
|
159
|
+
entries.append(manifest)
|
|
160
|
+
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
161
|
+
entries.append({"id": path.stem, "complete": False, "error": str(error)})
|
|
162
|
+
sessions = self.root.parent / "sessions"
|
|
163
|
+
if sessions.is_dir():
|
|
164
|
+
for session in sorted(sessions.iterdir()):
|
|
165
|
+
if not session.is_dir() or (thread is not None and session.name != thread):
|
|
166
|
+
continue
|
|
167
|
+
for pattern, source in (
|
|
168
|
+
("pre-compact-*.jsonl", "manual"),
|
|
169
|
+
("conversation_history/*.md", "automatic"),
|
|
170
|
+
):
|
|
171
|
+
entries.extend(
|
|
172
|
+
{
|
|
173
|
+
"id": f"legacy:{legacy.relative_to(sessions).as_posix()}",
|
|
174
|
+
"thread": session.name,
|
|
175
|
+
"source": source,
|
|
176
|
+
"path": str(legacy),
|
|
177
|
+
"complete": False,
|
|
178
|
+
"restorable": False,
|
|
179
|
+
"error": "Legacy transcript lacks a complete checkpoint manifest",
|
|
180
|
+
}
|
|
181
|
+
for legacy in sorted(session.glob(pattern))
|
|
182
|
+
if legacy.is_file()
|
|
183
|
+
)
|
|
184
|
+
return entries
|
|
185
|
+
|
|
186
|
+
async def restore(self, reference: str, agent: Any) -> str:
|
|
187
|
+
"""Restore messages and graph approvals into a fresh checkpoint thread."""
|
|
188
|
+
import asyncio
|
|
189
|
+
|
|
190
|
+
_manifest, payload = await asyncio.to_thread(self.inspect, reference)
|
|
191
|
+
thread = uuid.uuid4().hex
|
|
192
|
+
config = {"configurable": {"thread_id": thread, "checkpoint_ns": ""}}
|
|
193
|
+
state = dict(payload["state"])
|
|
194
|
+
state["messages"] = messages_from_dict(payload["messages"])
|
|
195
|
+
# Persist approval interrupts through the saver, rather than advancing
|
|
196
|
+
# the graph's model node and losing the pending task/approval structure.
|
|
197
|
+
if payload.get("checkpoint"):
|
|
198
|
+
from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
|
|
199
|
+
|
|
200
|
+
if not isinstance(agent, CoordinatedGraph):
|
|
201
|
+
message = "Checkpoint restoration requires a coordinated graph"
|
|
202
|
+
raise RecoveryArchiveError(message)
|
|
203
|
+
saver = agent.coordinator.saver
|
|
204
|
+
packed = payload["checkpoint"]
|
|
205
|
+
checkpoint = saver.serde.loads_typed(tuple(packed["checkpoint"]))
|
|
206
|
+
pending = saver.serde.loads_typed(tuple(packed["pending"]))
|
|
207
|
+
async with agent.coordinator.mutation(config, agent.project):
|
|
208
|
+
saved = await saver.aput(
|
|
209
|
+
config, checkpoint, packed["metadata"], checkpoint["channel_versions"]
|
|
210
|
+
)
|
|
211
|
+
grouped: dict[str, list[tuple[str, Any]]] = {}
|
|
212
|
+
for task_id, channel, value in pending:
|
|
213
|
+
grouped.setdefault(task_id, []).append((channel, value))
|
|
214
|
+
for task_id, writes in grouped.items():
|
|
215
|
+
await saver.aput_writes(saved, writes, task_id)
|
|
216
|
+
return thread
|
|
217
|
+
if payload["approvals"]:
|
|
218
|
+
message = "Archive contains pending approvals; use checkpoint-preserving restore"
|
|
219
|
+
raise RecoveryArchiveError(message)
|
|
220
|
+
await agent.aupdate_state(config, state)
|
|
221
|
+
return thread
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def default_recovery_store() -> RecoveryStore:
|
|
225
|
+
"""Resolve the shared local archive store."""
|
|
226
|
+
from novacode_cli.config import config
|
|
227
|
+
|
|
228
|
+
return RecoveryStore(config.HOME_DIR / "recovery")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def handle_recovery_command(args: str, *, agent: Any, thread: str) -> tuple[str, str | None]:
|
|
232
|
+
"""Transport-independent listing, inspection, and fresh-thread restoration."""
|
|
233
|
+
import asyncio
|
|
234
|
+
|
|
235
|
+
parts = args.split()
|
|
236
|
+
store = default_recovery_store()
|
|
237
|
+
if not parts or parts == ["list"]:
|
|
238
|
+
entries = await asyncio.to_thread(store.list, thread)
|
|
239
|
+
return json.dumps(entries, indent=2), None
|
|
240
|
+
command_arity = 2
|
|
241
|
+
if len(parts) == command_arity and parts[0] == "inspect":
|
|
242
|
+
manifest, _payload = await asyncio.to_thread(store.inspect, parts[1])
|
|
243
|
+
return json.dumps(manifest, indent=2), None
|
|
244
|
+
if len(parts) == command_arity and parts[0] == "restore":
|
|
245
|
+
restored = await store.restore(parts[1], agent)
|
|
246
|
+
return f"Restored into new thread {restored}", restored
|
|
247
|
+
message = "Usage: /context recovery [list|inspect <id>|restore <id>]"
|
|
248
|
+
raise ValueError(message)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Separate text estimates, fixed image heuristics and image transport size."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from langchain_core.messages import AnyMessage
|
|
13
|
+
|
|
14
|
+
IMAGE_TYPES = frozenset({"image", "image_url", "input_image"})
|
|
15
|
+
TOKENS_PER_IMAGE = 85
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ContentEstimate:
|
|
20
|
+
"""Heuristics only; image dimensions and provider billing are unobserved."""
|
|
21
|
+
|
|
22
|
+
text_tokens: int
|
|
23
|
+
image_tokens: int
|
|
24
|
+
image_transport_chars: int
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def total(self) -> int:
|
|
28
|
+
"""Input estimate excluding schemas, with no base64 character charge."""
|
|
29
|
+
return self.text_tokens + self.image_tokens
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def estimate_content(messages: Sequence[AnyMessage]) -> ContentEstimate:
|
|
33
|
+
"""Count images separately, including nested tool-result image blocks."""
|
|
34
|
+
images = transport = 0
|
|
35
|
+
|
|
36
|
+
def sanitize(value: Any) -> Any: # noqa: ANN401 - heterogeneous content blocks
|
|
37
|
+
nonlocal images, transport
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return [sanitize(item) for item in value]
|
|
40
|
+
if not isinstance(value, Mapping):
|
|
41
|
+
return value
|
|
42
|
+
if value.get("type") in IMAGE_TYPES:
|
|
43
|
+
images += 1
|
|
44
|
+
payload = value.get("base64") or value.get("image_url") or value.get("url")
|
|
45
|
+
if isinstance(payload, Mapping):
|
|
46
|
+
payload = payload.get("url") or payload.get("data")
|
|
47
|
+
source = value.get("source")
|
|
48
|
+
if not payload and isinstance(source, Mapping):
|
|
49
|
+
payload = source.get("data")
|
|
50
|
+
if isinstance(payload, str):
|
|
51
|
+
if payload.startswith("data:"):
|
|
52
|
+
transport += len(payload.partition(",")[2])
|
|
53
|
+
elif not payload.startswith(("https://", "http://")):
|
|
54
|
+
transport += len(payload)
|
|
55
|
+
return {"type": "image"}
|
|
56
|
+
return {key: sanitize(item) for key, item in value.items()}
|
|
57
|
+
|
|
58
|
+
sanitized = [
|
|
59
|
+
message.model_copy(update={"content": sanitize(message.content)})
|
|
60
|
+
if isinstance(message.content, list)
|
|
61
|
+
else message
|
|
62
|
+
for message in messages
|
|
63
|
+
]
|
|
64
|
+
text_tokens = count_tokens_approximately(sanitized, tokens_per_image=0)
|
|
65
|
+
return ContentEstimate(text_tokens, images * TOKENS_PER_IMAGE, transport)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Reconstruct the conversation sent to the model after library compaction."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def effective_messages(values: Mapping[str, Any]) -> list[Any]:
|
|
8
|
+
"""Use the summary and retained tail instead of the checkpoint archive."""
|
|
9
|
+
messages = list(values.get("messages", []))
|
|
10
|
+
event = values.get("_summarization_event")
|
|
11
|
+
if not isinstance(event, Mapping):
|
|
12
|
+
return messages
|
|
13
|
+
cutoff = event.get("cutoff_index")
|
|
14
|
+
summary = event.get("summary_message")
|
|
15
|
+
if not isinstance(cutoff, int) or cutoff < 0 or summary is None:
|
|
16
|
+
return messages
|
|
17
|
+
return [summary, *messages[cutoff:]]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Disposable, owner-private SQLite knowledge artifacts and producer fences."""
|
|
2
|
+
# ruff: noqa: ANN401, EM101, TRY003
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import csv
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sqlite3
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from threading import RLock
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
TTL = 7 * 24 * 3600
|
|
19
|
+
MAX_ARTIFACTS = 2000
|
|
20
|
+
MAX_PAYLOAD = 64 * 1024 * 1024
|
|
21
|
+
LEASE_SECONDS = 90
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def digest(value: str | bytes) -> str:
|
|
25
|
+
"""Hash identities or complete source bytes without storing the original."""
|
|
26
|
+
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _protect(directory: Path) -> None:
|
|
30
|
+
"""Restrict the directory before creating SQLite and its WAL sidecars."""
|
|
31
|
+
directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
32
|
+
if directory.is_symlink() or directory.resolve() != directory.absolute():
|
|
33
|
+
raise ValueError("Knowledge directory must not contain links")
|
|
34
|
+
if os.name != "nt":
|
|
35
|
+
directory.chmod(0o700)
|
|
36
|
+
return
|
|
37
|
+
executable = Path(os.environ.get("SYSTEMROOT", "C:/Windows")) / "System32/icacls.exe"
|
|
38
|
+
identity = subprocess.run( # noqa: S603
|
|
39
|
+
[str(executable.with_name("whoami.exe")), "/user", "/fo", "csv", "/nh"],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=10,
|
|
43
|
+
check=True,
|
|
44
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
45
|
+
)
|
|
46
|
+
sid = next(csv.reader([identity.stdout.strip()]))[1]
|
|
47
|
+
if not sid.startswith("S-1-") or any(c not in "S0123456789-" for c in sid):
|
|
48
|
+
raise OSError("Could not determine knowledge directory owner")
|
|
49
|
+
subprocess.run( # noqa: S603
|
|
50
|
+
[str(executable), str(directory), "/inheritance:r", "/grant:r", f"*{sid}:(OI)(CI)F"],
|
|
51
|
+
stdout=subprocess.DEVNULL,
|
|
52
|
+
stderr=subprocess.DEVNULL,
|
|
53
|
+
timeout=10,
|
|
54
|
+
check=True,
|
|
55
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class KnowledgeCache:
|
|
60
|
+
"""Short independent transactions; never hold a connection across model work."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, directory: Path) -> None:
|
|
63
|
+
"""Defer filesystem and database work until a lookup or command."""
|
|
64
|
+
self.directory = directory.absolute()
|
|
65
|
+
self._ready = False
|
|
66
|
+
self._lock = RLock()
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def connection(self) -> Any:
|
|
70
|
+
"""Serialize lazy initialization and short writes within this instance."""
|
|
71
|
+
with self._lock, self._connection() as db:
|
|
72
|
+
yield db
|
|
73
|
+
|
|
74
|
+
@contextmanager
|
|
75
|
+
def _connection(self) -> Any:
|
|
76
|
+
"""Open a bounded writer transaction on a private disposable database."""
|
|
77
|
+
if not self._ready:
|
|
78
|
+
_protect(self.directory)
|
|
79
|
+
path = self.directory / "knowledge.sqlite3"
|
|
80
|
+
if path.is_symlink():
|
|
81
|
+
raise ValueError("Knowledge database must not be a link")
|
|
82
|
+
db = sqlite3.connect(path, timeout=0.5)
|
|
83
|
+
db.row_factory = sqlite3.Row
|
|
84
|
+
try:
|
|
85
|
+
if not self._ready:
|
|
86
|
+
self._wal(db)
|
|
87
|
+
db.executescript("""
|
|
88
|
+
CREATE TABLE IF NOT EXISTS namespaces (
|
|
89
|
+
ns TEXT PRIMARY KEY, epoch INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
semantic_cache_enabled INTEGER NOT NULL DEFAULT 1,
|
|
91
|
+
stats TEXT NOT NULL DEFAULT '{}');
|
|
92
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
93
|
+
ns TEXT NOT NULL, key TEXT NOT NULL, refs TEXT NOT NULL,
|
|
94
|
+
summary TEXT NOT NULL, model TEXT NOT NULL,
|
|
95
|
+
embedding TEXT NOT NULL, vector TEXT, created REAL NOT NULL,
|
|
96
|
+
expires REAL NOT NULL, accessed REAL NOT NULL, bytes INTEGER NOT NULL,
|
|
97
|
+
PRIMARY KEY(ns,key));
|
|
98
|
+
CREATE TABLE IF NOT EXISTS leases (
|
|
99
|
+
ns TEXT NOT NULL, key TEXT NOT NULL, epoch INTEGER NOT NULL,
|
|
100
|
+
owner TEXT NOT NULL, until REAL NOT NULL, PRIMARY KEY(ns,key));
|
|
101
|
+
CREATE INDEX IF NOT EXISTS artifact_access ON artifacts(accessed);
|
|
102
|
+
""")
|
|
103
|
+
self._ready = True
|
|
104
|
+
db.execute("BEGIN IMMEDIATE")
|
|
105
|
+
yield db
|
|
106
|
+
db.commit()
|
|
107
|
+
except BaseException:
|
|
108
|
+
db.rollback()
|
|
109
|
+
raise
|
|
110
|
+
finally:
|
|
111
|
+
db.close()
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _wal(db: sqlite3.Connection) -> None:
|
|
115
|
+
"""Bound retries for SQLite's non-waiting initial journal-mode upgrade."""
|
|
116
|
+
deadline = time.monotonic() + 0.5
|
|
117
|
+
while True:
|
|
118
|
+
try:
|
|
119
|
+
db.execute("PRAGMA journal_mode=WAL")
|
|
120
|
+
return # noqa: TRY300
|
|
121
|
+
except sqlite3.OperationalError as exc:
|
|
122
|
+
if (
|
|
123
|
+
getattr(exc, "sqlite_errorcode", 0)
|
|
124
|
+
not in {sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED}
|
|
125
|
+
or time.monotonic() >= deadline
|
|
126
|
+
):
|
|
127
|
+
raise
|
|
128
|
+
time.sleep(0.025)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def namespace(root: Path, assistant: str) -> str:
|
|
132
|
+
"""Canonical workspace plus assistant identity is the sharing boundary."""
|
|
133
|
+
return digest(json.dumps([os.path.normcase(str(root.resolve())), assistant]))
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _state(db: sqlite3.Connection, ns: str) -> sqlite3.Row:
|
|
137
|
+
db.execute("INSERT OR IGNORE INTO namespaces(ns) VALUES (?)", (ns,))
|
|
138
|
+
return db.execute("SELECT * FROM namespaces WHERE ns=?", (ns,)).fetchone()
|
|
139
|
+
|
|
140
|
+
def state(self, ns: str) -> dict:
|
|
141
|
+
"""Read the immediately authoritative enable flag and epoch."""
|
|
142
|
+
with self.connection() as db:
|
|
143
|
+
return dict(self._state(db, ns))
|
|
144
|
+
|
|
145
|
+
def control(self, ns: str, action: str) -> dict:
|
|
146
|
+
"""Clear/off fence old producers; each namespace retains its own toggle."""
|
|
147
|
+
with self.connection() as db:
|
|
148
|
+
self._state(db, ns)
|
|
149
|
+
if action in {"clear", "off"}:
|
|
150
|
+
db.execute("UPDATE namespaces SET epoch=epoch+1 WHERE ns=?", (ns,))
|
|
151
|
+
db.execute("DELETE FROM artifacts WHERE ns=?", (ns,))
|
|
152
|
+
db.execute("DELETE FROM leases WHERE ns=?", (ns,))
|
|
153
|
+
if action in {"on", "off"}:
|
|
154
|
+
db.execute(
|
|
155
|
+
"UPDATE namespaces SET semantic_cache_enabled=? WHERE ns=?",
|
|
156
|
+
(int(action == "on"), ns),
|
|
157
|
+
)
|
|
158
|
+
self._evict(db, ns)
|
|
159
|
+
state = self._state(db, ns)
|
|
160
|
+
stats = json.loads(state["stats"])
|
|
161
|
+
retained = db.execute(
|
|
162
|
+
"SELECT coalesce(sum(bytes),0) FROM artifacts WHERE ns=?", (ns,)
|
|
163
|
+
).fetchone()[0]
|
|
164
|
+
return {
|
|
165
|
+
"semantic_cache_enabled": bool(state["semantic_cache_enabled"]),
|
|
166
|
+
"epoch": state["epoch"],
|
|
167
|
+
"retained_bytes": retained,
|
|
168
|
+
**stats,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def event(self, ns: str, name: str, value: float = 1) -> None:
|
|
172
|
+
"""Persist only numeric accounting, never queries or source content."""
|
|
173
|
+
with self.connection() as db:
|
|
174
|
+
stats = json.loads(self._state(db, ns)["stats"])
|
|
175
|
+
stats[name] = stats.get(name, 0) + value
|
|
176
|
+
db.execute("UPDATE namespaces SET stats=? WHERE ns=?", (json.dumps(stats), ns))
|
|
177
|
+
|
|
178
|
+
def candidates(self, ns: str, model: str, key: str | None) -> list[dict]:
|
|
179
|
+
"""Retrieve bounded metadata; callers must validate every returned source."""
|
|
180
|
+
with self.connection() as db:
|
|
181
|
+
self._evict(db, ns)
|
|
182
|
+
rows = db.execute(
|
|
183
|
+
"SELECT * FROM artifacts WHERE ns=? AND model=?" + (" AND key=?" if key else ""), # noqa: S608 - fixed SQL suffix.
|
|
184
|
+
(ns, model, key) if key else (ns, model),
|
|
185
|
+
).fetchall()
|
|
186
|
+
return [dict(row) for row in rows]
|
|
187
|
+
|
|
188
|
+
def touch(self, ns: str, key: str, epoch: int) -> bool:
|
|
189
|
+
"""Validate the fence and update access after source validation."""
|
|
190
|
+
with self.connection() as db:
|
|
191
|
+
state = self._state(db, ns)
|
|
192
|
+
if state["epoch"] != epoch or not state["semantic_cache_enabled"]:
|
|
193
|
+
return False
|
|
194
|
+
return bool(
|
|
195
|
+
db.execute(
|
|
196
|
+
"UPDATE artifacts SET accessed=? WHERE ns=? AND key=?",
|
|
197
|
+
(self._access(db), ns, key),
|
|
198
|
+
).rowcount
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def invalidate(self, ns: str, key: str) -> None:
|
|
202
|
+
"""Discard a source-invalid artifact."""
|
|
203
|
+
with self.connection() as db:
|
|
204
|
+
db.execute("DELETE FROM artifacts WHERE ns=? AND key=?", (ns, key))
|
|
205
|
+
self.event(ns, "invalidations")
|
|
206
|
+
|
|
207
|
+
def acquire(self, ns: str, key: str, epoch: int, owner: str) -> bool:
|
|
208
|
+
"""Lease a single producer across processes, expiring crashed producers."""
|
|
209
|
+
with self.connection() as db:
|
|
210
|
+
state = self._state(db, ns)
|
|
211
|
+
if state["epoch"] != epoch or not state["semantic_cache_enabled"]:
|
|
212
|
+
return False
|
|
213
|
+
db.execute("DELETE FROM leases WHERE until<=?", (time.time(),))
|
|
214
|
+
return bool(
|
|
215
|
+
db.execute(
|
|
216
|
+
"INSERT OR IGNORE INTO leases VALUES (?,?,?,?,?)",
|
|
217
|
+
(ns, key, epoch, owner, time.time() + LEASE_SECONDS),
|
|
218
|
+
).rowcount
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def release(self, ns: str, key: str, owner: str) -> None:
|
|
222
|
+
"""Release only this producer's lease, never a replacement lease."""
|
|
223
|
+
with self.connection() as db:
|
|
224
|
+
db.execute("DELETE FROM leases WHERE ns=? AND key=? AND owner=?", (ns, key, owner))
|
|
225
|
+
|
|
226
|
+
def publish(
|
|
227
|
+
self,
|
|
228
|
+
ns: str,
|
|
229
|
+
key: str,
|
|
230
|
+
epoch: int,
|
|
231
|
+
owner: str,
|
|
232
|
+
refs: str,
|
|
233
|
+
summary: str,
|
|
234
|
+
model: str,
|
|
235
|
+
embedding: str,
|
|
236
|
+
vector: list[float] | None,
|
|
237
|
+
) -> bool:
|
|
238
|
+
"""Commit only while both the namespace fence and producer lease match."""
|
|
239
|
+
payload = json.dumps(vector, allow_nan=False) if vector is not None else None
|
|
240
|
+
size = len((refs + summary + model + embedding + (payload or "")).encode())
|
|
241
|
+
with self.connection() as db:
|
|
242
|
+
state = self._state(db, ns)
|
|
243
|
+
lease = db.execute("SELECT * FROM leases WHERE ns=? AND key=?", (ns, key)).fetchone()
|
|
244
|
+
if (
|
|
245
|
+
state["epoch"] != epoch
|
|
246
|
+
or not state["semantic_cache_enabled"]
|
|
247
|
+
or not lease
|
|
248
|
+
or lease["owner"] != owner
|
|
249
|
+
or lease["until"] <= time.time()
|
|
250
|
+
):
|
|
251
|
+
return False
|
|
252
|
+
now = time.time()
|
|
253
|
+
db.execute(
|
|
254
|
+
"INSERT OR REPLACE INTO artifacts VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
255
|
+
(
|
|
256
|
+
ns,
|
|
257
|
+
key,
|
|
258
|
+
refs,
|
|
259
|
+
summary,
|
|
260
|
+
model,
|
|
261
|
+
embedding,
|
|
262
|
+
payload,
|
|
263
|
+
now,
|
|
264
|
+
now + TTL,
|
|
265
|
+
self._access(db),
|
|
266
|
+
size,
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
self._evict(db, ns)
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _access(db: sqlite3.Connection) -> int:
|
|
274
|
+
"""Order touches even when the platform clock repeats or moves backwards."""
|
|
275
|
+
previous = db.execute("SELECT coalesce(max(accessed),0) FROM artifacts").fetchone()[0]
|
|
276
|
+
return max(time.time_ns() // 1000, int(previous) + 1)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _evict(db: sqlite3.Connection, ns: str) -> None:
|
|
280
|
+
db.execute("DELETE FROM artifacts WHERE expires<=?", (time.time(),))
|
|
281
|
+
db.execute(
|
|
282
|
+
"DELETE FROM artifacts WHERE ns=? AND key IN "
|
|
283
|
+
"(SELECT key FROM artifacts WHERE ns=? ORDER BY accessed DESC LIMIT -1 OFFSET ?)",
|
|
284
|
+
(ns, ns, MAX_ARTIFACTS),
|
|
285
|
+
)
|
|
286
|
+
total = db.execute("SELECT coalesce(sum(bytes),0) FROM artifacts").fetchone()[0]
|
|
287
|
+
while total > MAX_PAYLOAD:
|
|
288
|
+
row = db.execute(
|
|
289
|
+
"SELECT ns,key,bytes FROM artifacts ORDER BY accessed LIMIT 1"
|
|
290
|
+
).fetchone()
|
|
291
|
+
db.execute("DELETE FROM artifacts WHERE ns=? AND key=?", (row["ns"], row["key"]))
|
|
292
|
+
total -= row["bytes"]
|