gemcode 0.4.25__tar.gz → 0.4.26__tar.gz
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.
- {gemcode-0.4.25/src/gemcode.egg-info → gemcode-0.4.26}/PKG-INFO +1 -1
- {gemcode-0.4.25 → gemcode-0.4.26}/pyproject.toml +1 -1
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/hitl_bridge.py +72 -15
- {gemcode-0.4.25 → gemcode-0.4.26/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode.egg-info/SOURCES.txt +1 -0
- gemcode-0.4.26/tests/test_hitl_bridge_cross_process.py +53 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/LICENSE +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/MANIFEST.in +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/README.md +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/setup.cfg +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/agent_mesh.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/config.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/habit_chains.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/habit_runs.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/org.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/version.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/chat_skills.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/customize_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/files_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/fleet_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/preview_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/project_root.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/runtime_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/serve_bind.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/serve_state.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/server.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/sessions_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/terminal_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/ui_chat_store.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/web_config_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/web/workspace_panel_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_agent_mesh.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_automations.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_credentials.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_files_api.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_habit_chains.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_habit_runs.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_hosted_tenant.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_paths.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_permissions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_preview_proxy.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_skills.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_sse_confirmation_batch.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_tools.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_web_permissions_hitl.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_web_server.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.25 → gemcode-0.4.26}/tests/test_workspace_hints.py +0 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""File-based HITL approval bridge for web chat (subprocess-safe).
|
|
1
|
+
"""File-based HITL approval bridge for web chat (subprocess-safe).
|
|
2
|
+
|
|
3
|
+
Chat SSE runs in a ``gemcode.web.sse_adapter`` subprocess while
|
|
4
|
+
``POST /api/chat/approve`` is handled by the parent ``gemcode serve`` process.
|
|
5
|
+
In-memory waiter sets therefore cannot detect “live” approvals across that
|
|
6
|
+
boundary — use ``*.waiting`` marker files on the shared approval dir instead.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
@@ -17,22 +23,64 @@ def _approval_dir() -> Path:
|
|
|
17
23
|
return Path.home() / ".gemcode" / "web_approvals"
|
|
18
24
|
_DEFAULT_TIMEOUT_S = float(os.environ.get("GEMCODE_WEB_HITL_TIMEOUT_S", "3600"))
|
|
19
25
|
_POLL_INTERVAL_S = 0.25
|
|
20
|
-
#
|
|
26
|
+
# Same-process hints only (parent server never shares these with sse_adapter).
|
|
21
27
|
_active_waiters: set[str] = set()
|
|
22
|
-
# emitted to the UI but wait_for_web_approval not started yet (preflight)
|
|
23
28
|
_pending_approval_ids: set[str] = set()
|
|
24
29
|
|
|
25
30
|
|
|
31
|
+
def _safe_id(approval_id: str) -> str:
|
|
32
|
+
return "".join(c if c.isalnum() or c in "-_:" else "_" for c in approval_id)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _approval_path(approval_id: str) -> Path:
|
|
36
|
+
return _approval_dir() / f"{_safe_id(approval_id)}.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _waiting_path(approval_id: str) -> Path:
|
|
40
|
+
"""Marker written by the SSE subprocess while an approval is expected/active."""
|
|
41
|
+
return _approval_dir() / f"{_safe_id(approval_id)}.waiting"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_live_approval(approval_id: str) -> bool:
|
|
45
|
+
"""True if a chat turn is still waiting for this approval (cross-process safe)."""
|
|
46
|
+
aid = (approval_id or "").strip()
|
|
47
|
+
if not aid:
|
|
48
|
+
return False
|
|
49
|
+
if aid in _active_waiters or aid in _pending_approval_ids:
|
|
50
|
+
return True
|
|
51
|
+
try:
|
|
52
|
+
path = _waiting_path(aid)
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
return False
|
|
55
|
+
# Heartbeats refresh mtime; stale markers mean the SSE subprocess died.
|
|
56
|
+
age_s = time.time() - path.stat().st_mtime
|
|
57
|
+
return age_s < 90.0
|
|
58
|
+
except OSError:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
26
62
|
def register_pending_approval(approval_id: str) -> None:
|
|
27
63
|
"""Mark an approval as expected so early UI clicks are not reported as late."""
|
|
28
64
|
aid = (approval_id or "").strip()
|
|
29
|
-
if aid:
|
|
30
|
-
|
|
65
|
+
if not aid:
|
|
66
|
+
return
|
|
67
|
+
_pending_approval_ids.add(aid)
|
|
68
|
+
_touch_waiting(aid)
|
|
31
69
|
|
|
32
70
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
def _touch_waiting(approval_id: str) -> None:
|
|
72
|
+
try:
|
|
73
|
+
_approval_dir().mkdir(parents=True, exist_ok=True)
|
|
74
|
+
_waiting_path(approval_id).write_text(f"{time.time():.3f}\n", encoding="utf-8")
|
|
75
|
+
except OSError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _clear_waiting_marker(approval_id: str) -> None:
|
|
80
|
+
try:
|
|
81
|
+
_waiting_path(approval_id).unlink(missing_ok=True)
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
36
84
|
|
|
37
85
|
|
|
38
86
|
def _read_approval_file(path: Path) -> bool | None:
|
|
@@ -60,14 +108,16 @@ def resolve_web_approval(approval_id: str, *, confirmed: bool) -> dict[str, Any]
|
|
|
60
108
|
return {"ok": False, "error": "approval_id is required"}
|
|
61
109
|
_approval_dir().mkdir(parents=True, exist_ok=True)
|
|
62
110
|
path = _approval_path(approval_id)
|
|
111
|
+
# Detect live wait BEFORE writing; marker is owned by the SSE subprocess.
|
|
112
|
+
live = _is_live_approval(approval_id)
|
|
63
113
|
payload = {"confirmed": bool(confirmed), "resolved_ms": int(time.time() * 1000)}
|
|
64
114
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
65
|
-
late = approval_id not in _active_waiters and approval_id not in _pending_approval_ids
|
|
66
115
|
return {
|
|
67
116
|
"ok": True,
|
|
68
117
|
"approval_id": approval_id,
|
|
69
118
|
"confirmed": bool(confirmed),
|
|
70
|
-
|
|
119
|
+
# late = no waiter — UI may start a recovery turn. Never true while SSE waits.
|
|
120
|
+
"late": not live,
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
|
|
@@ -81,14 +131,18 @@ async def wait_for_web_approval(
|
|
|
81
131
|
"""Block until the user approves/denies or timeout (deny on timeout)."""
|
|
82
132
|
_approval_dir().mkdir(parents=True, exist_ok=True)
|
|
83
133
|
path = _approval_path(approval_id)
|
|
134
|
+
register_pending_approval(approval_id)
|
|
84
135
|
|
|
85
136
|
# User may have approved before we started waiting (preflight UI is immediate).
|
|
86
137
|
existing = _read_approval_file(path)
|
|
87
138
|
if existing is not None:
|
|
139
|
+
_pending_approval_ids.discard(approval_id)
|
|
140
|
+
_clear_waiting_marker(approval_id)
|
|
88
141
|
return existing
|
|
89
142
|
|
|
90
143
|
_pending_approval_ids.discard(approval_id)
|
|
91
144
|
_active_waiters.add(approval_id)
|
|
145
|
+
_touch_waiting(approval_id)
|
|
92
146
|
try:
|
|
93
147
|
deadline = time.monotonic() + max(1.0, timeout_s)
|
|
94
148
|
next_hb = time.monotonic()
|
|
@@ -98,17 +152,20 @@ async def wait_for_web_approval(
|
|
|
98
152
|
if result is not None:
|
|
99
153
|
return result
|
|
100
154
|
now = time.monotonic()
|
|
101
|
-
if
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
155
|
+
if now >= next_hb:
|
|
156
|
+
_touch_waiting(approval_id)
|
|
157
|
+
if heartbeat is not None:
|
|
158
|
+
try:
|
|
159
|
+
heartbeat()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
106
162
|
next_hb = now + max(5.0, heartbeat_s)
|
|
107
163
|
await asyncio.sleep(_POLL_INTERVAL_S)
|
|
108
164
|
return False
|
|
109
165
|
finally:
|
|
110
166
|
_active_waiters.discard(approval_id)
|
|
111
167
|
_pending_approval_ids.discard(approval_id)
|
|
168
|
+
_clear_waiting_marker(approval_id)
|
|
112
169
|
|
|
113
170
|
|
|
114
171
|
def new_approval_id(session_id: str, fc_id: str | None) -> str:
|
|
@@ -174,6 +174,7 @@ tests/test_files_api.py
|
|
|
174
174
|
tests/test_fleet_reports.py
|
|
175
175
|
tests/test_habit_chains.py
|
|
176
176
|
tests/test_habit_runs.py
|
|
177
|
+
tests/test_hitl_bridge_cross_process.py
|
|
177
178
|
tests/test_hosted_tenant.py
|
|
178
179
|
tests/test_ide_stdio_attachments.py
|
|
179
180
|
tests/test_interactive_permission_ask.py
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Cross-process HITL late detection (parent approve vs sse_adapter wait)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from gemcode.web import hitl_bridge
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_resolve_not_late_when_waiting_marker_exists(tmp_path: Path, monkeypatch) -> None:
|
|
12
|
+
monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(tmp_path))
|
|
13
|
+
aid = "sess:tool1"
|
|
14
|
+
hitl_bridge.register_pending_approval(aid)
|
|
15
|
+
assert (tmp_path / ".gemcode" / "web_approvals" / "sess:tool1.waiting").is_file()
|
|
16
|
+
|
|
17
|
+
# Simulate parent process: clear in-memory sets (as if another process).
|
|
18
|
+
hitl_bridge._pending_approval_ids.clear()
|
|
19
|
+
hitl_bridge._active_waiters.clear()
|
|
20
|
+
|
|
21
|
+
out = hitl_bridge.resolve_web_approval(aid, confirmed=True)
|
|
22
|
+
assert out["ok"] is True
|
|
23
|
+
assert out["late"] is False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_resolve_late_when_no_waiter(tmp_path: Path, monkeypatch) -> None:
|
|
27
|
+
monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(tmp_path))
|
|
28
|
+
hitl_bridge._pending_approval_ids.clear()
|
|
29
|
+
hitl_bridge._active_waiters.clear()
|
|
30
|
+
out = hitl_bridge.resolve_web_approval("sess:orphan", confirmed=True)
|
|
31
|
+
assert out["ok"] is True
|
|
32
|
+
assert out["late"] is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_wait_picks_up_parent_resolve(tmp_path: Path, monkeypatch) -> None:
|
|
36
|
+
monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(tmp_path))
|
|
37
|
+
aid = "sess:bash1"
|
|
38
|
+
|
|
39
|
+
async def _run() -> bool:
|
|
40
|
+
async def approve_soon() -> None:
|
|
41
|
+
await asyncio.sleep(0.35)
|
|
42
|
+
# Parent server process has empty in-memory waiter sets.
|
|
43
|
+
hitl_bridge._pending_approval_ids.clear()
|
|
44
|
+
hitl_bridge._active_waiters.clear()
|
|
45
|
+
out = hitl_bridge.resolve_web_approval(aid, confirmed=True)
|
|
46
|
+
assert out["late"] is False # *.waiting marker from waiter process
|
|
47
|
+
|
|
48
|
+
task = asyncio.create_task(approve_soon())
|
|
49
|
+
confirmed = await hitl_bridge.wait_for_web_approval(aid, timeout_s=5.0)
|
|
50
|
+
await task
|
|
51
|
+
return confirmed
|
|
52
|
+
|
|
53
|
+
assert asyncio.run(_run()) is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|