execweave 0.8.32__tar.gz → 0.8.33__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.
Files changed (196) hide show
  1. {execweave-0.8.32 → execweave-0.8.33}/PKG-INFO +1 -1
  2. {execweave-0.8.32 → execweave-0.8.33}/pyproject.toml +2 -2
  3. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/__init__.py +1 -1
  4. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/_dashboard_shell_base.py +1 -0
  5. execweave-0.8.33/src/execweave/acceptance_output.py +19 -0
  6. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_adapter.py +2 -1
  7. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_hook_cli.py +26 -17
  8. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_hook_entry.py +3 -1
  9. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/collector.py +3 -0
  10. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_records.py +4 -1
  11. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_records_common.py +1 -0
  12. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/dashboard_shell.py +8 -10
  13. execweave-0.8.33/src/execweave/finalization.py +44 -0
  14. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/graph.py +13 -0
  15. execweave-0.8.33/src/execweave/hook_input.py +16 -0
  16. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_core.py +51 -0
  17. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_script_a.py +13 -1
  18. execweave-0.8.33/src/execweave/session_summary.py +48 -0
  19. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel.py +47 -36
  20. execweave-0.8.33/src/execweave/viewer_background_files.py +86 -0
  21. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_flow_canvas.py +20 -1
  22. execweave-0.8.33/src/execweave/viewer_fold_state.py +31 -0
  23. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_semantic_projection.py +9 -2
  24. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_session_flow.py +1 -1
  25. {execweave-0.8.32 → execweave-0.8.33}/.gitignore +0 -0
  26. {execweave-0.8.32 → execweave-0.8.33}/LICENSE +0 -0
  27. {execweave-0.8.32 → execweave-0.8.33}/README.md +0 -0
  28. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/__main__.py +0 -0
  29. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/_conversation_records_core.py +0 -0
  30. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/_http_proxy_base.py +0 -0
  31. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/_http_proxy_bounded.py +0 -0
  32. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/_http_proxy_stage.py +0 -0
  33. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/agent_bootstrap.py +0 -0
  34. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/agent_topology.py +0 -0
  35. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/agent_trace.py +0 -0
  36. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/agy_preview_sanitize.py +0 -0
  37. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/analysis.py +0 -0
  38. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/anthropic.py +0 -0
  39. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/anthropic_cli.py +0 -0
  40. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/anthropic_full_fidelity.py +0 -0
  41. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_adapter.py +0 -0
  42. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_adapter_base.py +0 -0
  43. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_full_fidelity.py +0 -0
  44. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_full_fidelity_base.py +0 -0
  45. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_full_fidelity_collaboration_base.py +0 -0
  46. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_hook_cli.py +0 -0
  47. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_record.py +0 -0
  48. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_subagent_linkage.py +0 -0
  49. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/antigravity_trace_capability.py +0 -0
  50. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/auto_specialized.py +0 -0
  51. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/backends.py +0 -0
  52. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/benchmark.py +0 -0
  53. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_adapter.py +0 -0
  54. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_child_transcript.py +0 -0
  55. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_delegation.py +0 -0
  56. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_full_fidelity.py +0 -0
  57. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_hook_cli.py +0 -0
  58. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_hook_contract.py +0 -0
  59. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_model_observer.py +0 -0
  60. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/claude_record.py +0 -0
  61. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cli.py +0 -0
  62. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_conversation.py +0 -0
  63. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_full_fidelity.py +0 -0
  64. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_hook_lifecycle.py +0 -0
  65. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_message_diagnostics.py +0 -0
  66. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_message_transport_diagnostics.py +0 -0
  67. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_record.py +0 -0
  68. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_rollout_structures.py +0 -0
  69. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_rollout_trace.py +0 -0
  70. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/codex_rollout_trace_base.py +0 -0
  71. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/command.py +0 -0
  72. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/content_evidence.py +0 -0
  73. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/content_store.py +0 -0
  74. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_archive.py +0 -0
  75. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_message_identity.py +0 -0
  76. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview.py +0 -0
  77. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_antigravity.py +0 -0
  78. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_claude.py +0 -0
  79. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_codex.py +0 -0
  80. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_common.py +0 -0
  81. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_generic.py +0 -0
  82. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_lines.py +0 -0
  83. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_preview_transcript.py +0 -0
  84. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_records_antigravity.py +0 -0
  85. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_records_codex.py +0 -0
  86. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/conversation_records_ollama.py +0 -0
  87. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/correlation.py +0 -0
  88. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_adapter.py +0 -0
  89. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_delegation.py +0 -0
  90. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_delegation_base.py +0 -0
  91. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_full_fidelity.py +0 -0
  92. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_hook_cli.py +0 -0
  93. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_hook_contract.py +0 -0
  94. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_lifecycle.py +0 -0
  95. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/cursor_record.py +0 -0
  96. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/entry.py +0 -0
  97. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/evidence_availability.py +0 -0
  98. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/evidence_grade.py +0 -0
  99. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/fidelity.py +0 -0
  100. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/filesystem.py +0 -0
  101. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/focus.py +0 -0
  102. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/__init__.py +0 -0
  103. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/autogen.py +0 -0
  104. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/base.py +0 -0
  105. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/camel.py +0 -0
  106. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/camel_channel.py +0 -0
  107. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/metagpt.py +0 -0
  108. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/framework_adapters/registry.py +0 -0
  109. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/graph_ops.py +0 -0
  110. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/http_proxy.py +0 -0
  111. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/http_proxy_cli.py +0 -0
  112. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/inference_gateway.py +0 -0
  113. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/inference_gateway_cli.py +0 -0
  114. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/inference_gateway_full_fidelity.py +0 -0
  115. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/inference_identity.py +0 -0
  116. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/inference_identity_cli.py +0 -0
  117. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/integrity.py +0 -0
  118. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/integrity_cli.py +0 -0
  119. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/litellm_callback.py +0 -0
  120. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/litellm_callback_cli.py +0 -0
  121. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live.py +0 -0
  122. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view.py +0 -0
  123. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_extra_style.py +0 -0
  124. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_markup.py +0 -0
  125. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_process_layout.py +0 -0
  126. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_readability.py +0 -0
  127. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_script_b.py +0 -0
  128. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_script_c.py +0 -0
  129. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_script_d.py +0 -0
  130. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/live_view_style.py +0 -0
  131. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/model_runtime.py +0 -0
  132. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/model_runtime_cli.py +0 -0
  133. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/model_runtime_full_fidelity.py +0 -0
  134. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/openai_compatible.py +0 -0
  135. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/openai_compatible_cli.py +0 -0
  136. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/openai_compatible_full_fidelity.py +0 -0
  137. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_adapter.py +0 -0
  138. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_event_contract.py +0 -0
  139. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_full_fidelity.py +0 -0
  140. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_hook_cli.py +0 -0
  141. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_plugin_cli.py +0 -0
  142. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_record.py +0 -0
  143. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/opencode_task_linkage.py +0 -0
  144. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/overhead_benchmark.py +0 -0
  145. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/provider_capability.py +0 -0
  146. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/provider_lifecycle.py +0 -0
  147. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/provider_record.py +0 -0
  148. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/rule_pack.py +0 -0
  149. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/rule_pack_cli.py +0 -0
  150. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/runtime_endpoint_availability.py +0 -0
  151. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/scalability_benchmark.py +0 -0
  152. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/schema.py +0 -0
  153. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/scope.py +0 -0
  154. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/semantic.py +0 -0
  155. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/sink.py +0 -0
  156. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/strace_backend.py +0 -0
  157. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/stream_assembly.py +0 -0
  158. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/theme.py +0 -0
  159. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/top.py +0 -0
  160. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/top_cli.py +0 -0
  161. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/validate.py +0 -0
  162. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/vendor/dagre.min.js +0 -0
  163. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/view_cli.py +0 -0
  164. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer.py +0 -0
  165. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
  166. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_claude.py +0 -0
  167. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_codex.py +0 -0
  168. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_cursor.py +0 -0
  169. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_default.py +0 -0
  170. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_ollama.py +0 -0
  171. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_opencode.py +0 -0
  172. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_agent_panel_registry.py +0 -0
  173. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_content_inspector.py +0 -0
  174. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_dashboard_clean.py +0 -0
  175. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_dashboard_focus.py +0 -0
  176. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_dashboard_hardening.py +0 -0
  177. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
  178. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_dashboard_pr70.py +0 -0
  179. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_edge_decross.py +0 -0
  180. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_execution_flow.py +0 -0
  181. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_execution_flow_provider_normalization.py +0 -0
  182. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_execution_flow_safety.py +0 -0
  183. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_external_endpoints.py +0 -0
  184. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_flow_layout.py +0 -0
  185. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_framework_flow.py +0 -0
  186. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_gif.py +0 -0
  187. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_layout_geometry.py +0 -0
  188. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_layout_v2.py +0 -0
  189. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_layout_v2_detached.py +0 -0
  190. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_limits.py +0 -0
  191. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_live_layout.py +0 -0
  192. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_projection.py +0 -0
  193. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_projection_base.py +0 -0
  194. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_projection_bridges.py +0 -0
  195. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/viewer_tool_mirror_identity.py +0 -0
  196. {execweave-0.8.32 → execweave-0.8.33}/src/execweave/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execweave
3
- Version: 0.8.32
3
+ Version: 0.8.33
4
4
  Summary: Turn AI-agent runtime behavior into a local interactive execution graph.
5
5
  Project-URL: Homepage, https://github.com/Irish-kw/ExecWeave
6
6
  Project-URL: Repository, https://github.com/Irish-kw/ExecWeave
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execweave"
7
- # ExecWeave v0.8.32 release metadata
8
- version = "0.8.32"
7
+ # ExecWeave v0.8.33 release metadata
8
+ version = "0.8.33"
9
9
  description = "Turn AI-agent runtime behavior into a local interactive execution graph."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """ExecWeave runtime graph package."""
2
2
 
3
- __version__ = "0.8.32"
3
+ __version__ = "0.8.33"
@@ -118,6 +118,7 @@ def render_static_dashboard_html(
118
118
  static_start = (
119
119
  "applyTheme(initialTheme());applyTransform();"
120
120
  "setSnapshot(window.__execweaveStaticGraph||{});"
121
+ "updateEvidence(window.__execweaveStaticGraph||{});"
121
122
  "setStatus('FINISHED','finished');"
122
123
  "window.__execweaveDashboard?.onFinished?.();"
123
124
  )
@@ -0,0 +1,19 @@
1
+ """Safe output placement for workloads launched inside an active recording."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+
8
+ def prepare_acceptance_output(path: str | Path) -> Path:
9
+ output = Path(path).expanduser().resolve()
10
+ sidecar = os.environ.get("EXECWEAVE_SEMANTIC_SIDECAR")
11
+ shared = bool(sidecar and Path(sidecar).expanduser().resolve().parent == output
12
+ and os.environ.get("EXECWEAVE_SESSION_ID"))
13
+ if output.exists():
14
+ if not output.is_dir():
15
+ raise FileExistsError(f"Acceptance output is not a directory: {output}")
16
+ if (output / "summary.json").exists() or (any(output.iterdir()) and not shared):
17
+ raise FileExistsError(f"Acceptance output contains a previous run: {output}")
18
+ output.mkdir(parents=True, exist_ok=True)
19
+ return output
@@ -9,6 +9,7 @@ from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
  from .agent_topology import EVIDENCE_SUBAGENT_LIFECYCLE_HOOK, subagent_topology
12
+ from .hook_input import read_hook_text
12
13
 
13
14
  _MAX_COMMAND_CHARS = 4096
14
15
  _MAX_LABEL_CHARS = 160
@@ -443,7 +444,7 @@ def append_semantic_records(path: str | Path, records: list[dict[str, Any]]) ->
443
444
 
444
445
  def read_hook_payload(stream: Any = None) -> dict[str, Any]:
445
446
  source = stream if stream is not None else sys.stdin
446
- raw = source.read()
447
+ raw = read_hook_text(source)
447
448
  if not isinstance(raw, str) or not raw.strip():
448
449
  raise ValueError("Codex hook stdin is empty")
449
450
  try:
@@ -246,23 +246,32 @@ def main(argv: list[str] | None = None) -> int:
246
246
  failures=failures,
247
247
  )
248
248
 
249
- try:
250
- content_records = codex_hook_to_content_events(
251
- payload,
252
- store=content_store,
253
- timestamp=observed_at,
254
- include_metadata=False,
255
- )
256
- except _CAPTURE_ERRORS as exc:
257
- failures.append("content_capture")
258
- _capture_warning("content_capture", exc)
259
- else:
260
- _append_stage(
261
- sidecar,
262
- content_records,
263
- stage="content_append",
264
- failures=failures,
265
- )
249
+ # Content fields are independent evidence. A malformed input string must not
250
+ # prevent a valid tool result (or transcript archive) from being stored.
251
+ content_fields = {"prompt", "tool_input", "tool_response", "last_assistant_message"}
252
+ for field in sorted(content_fields & payload.keys()):
253
+ isolated = {key: value for key, value in payload.items()
254
+ if key not in content_fields or key == field}
255
+ try:
256
+ content_records = codex_hook_to_content_events(
257
+ isolated, store=content_store, timestamp=observed_at, include_metadata=False,
258
+ )
259
+ except _CAPTURE_ERRORS as exc:
260
+ failures.append("content_capture")
261
+ _capture_warning("content_capture:" + field, exc)
262
+ diagnostic = {
263
+ "timestamp": observed_at, "event_type": "semantic.codex.content.capture_failed",
264
+ "relation": "CONTENT_CAPTURE_FAILED",
265
+ "source": {"id": "agent:OpenAI Codex", "type": "agent", "name": "OpenAI Codex"},
266
+ "target": None,
267
+ "attributes": {"provider": "codex", "observed_field": field,
268
+ "error_type": type(exc).__name__, "tool_use_id": payload.get("tool_use_id"),
269
+ "codex_hook_event_name": payload.get("hook_event_name"),
270
+ "causal": False, "inferred": False},
271
+ }
272
+ _append_stage(sidecar, [diagnostic], stage="content_diagnostic_append", failures=failures)
273
+ else:
274
+ _append_stage(sidecar, content_records, stage="content_append", failures=failures)
266
275
 
267
276
  try:
268
277
  archive_records = codex_conversation_archive_events(
@@ -142,7 +142,9 @@ def _append_interrupt_fast(sidecar: Path, payload: dict[str, Any]) -> None:
142
142
 
143
143
  def _active_auto_payload() -> tuple[str, dict[str, Any] | None]:
144
144
  try:
145
- raw = sys.stdin.read()
145
+ from .hook_input import read_hook_text
146
+
147
+ raw = read_hook_text(sys.stdin)
146
148
  except (OSError, ValueError):
147
149
  # The passive entry boundary must remain fail-open even when stdin itself is
148
150
  # unavailable. The normal capture path will receive an empty stream and its
@@ -210,6 +210,9 @@ class RuntimeCollector:
210
210
  "collector_pid": os.getpid(),
211
211
  "backend": self.backend_name,
212
212
  "execweave_version": __version__,
213
+ "python_executable": sys.executable,
214
+ "python_version": sys.version.split()[0],
215
+ "package_path": str(Path(__file__).resolve().parent),
213
216
  },
214
217
  )
215
218
  )
@@ -230,7 +230,7 @@ def _merge_conversation_previews(entries: list[dict[str, Any]]) -> None:
230
230
  continue
231
231
  for message in preview.get("messages") or []:
232
232
  if isinstance(message, dict):
233
- message["content_role"] = role
233
+ message.setdefault("content_role", role)
234
234
 
235
235
  snapshots: list[tuple[dict[str, Any], dict[str, Any]]] = []
236
236
  for entry in entries:
@@ -326,6 +326,9 @@ def conversation_record_entries(
326
326
  preview["provider_native_id"] = root_id
327
327
  original_source_id = entry.get("source_id")
328
328
  if isinstance(original_source_id, str) and original_source_id != root_id:
329
+ for message in preview.get("messages") or []:
330
+ if isinstance(message, dict):
331
+ message["occurrence_id"] = original_source_id
329
332
  entry["evidence_source_id"] = original_source_id
330
333
  entry["evidence_source_type"] = entry.get("source_type")
331
334
  entry["source_id"] = root_id
@@ -5,6 +5,7 @@ from typing import Any
5
5
 
6
6
  def history_message_key(message: dict[str, Any]) -> tuple[object, ...]:
7
7
  return (
8
+ message.get("occurrence_id"),
8
9
  message.get("ordinal"),
9
10
  message.get("kind"),
10
11
  message.get("sender"),
@@ -117,13 +117,10 @@ def _defer_camera_takeover_until_node_drag(html: str) -> str:
117
117
 
118
118
 
119
119
  def _preserve_readable_initial_camera(html: str) -> str:
120
- """Keep first paint readable without changing the explicit whole-graph Fit action.
120
+ """Use the same whole-graph Fit semantics for initial and explicit fitting.
121
121
 
122
- The initial snapshot historically called the same whole-graph ``fit`` routine as
123
- the user-facing Fit button. Dense graphs can therefore arrive below readable
124
- screen-space size even though the camera is otherwise in manual mode. Give only
125
- the automatic first fit a 0.5 scale floor; an explicit Fit still defaults to the
126
- established 0.07 floor so the full graph remains available as an overview.
122
+ Readability is addressed by evidence-preserving display clusters, not by
123
+ silently clipping nodes while the toolbar claims the entire graph fits.
127
124
  """
128
125
  signature = "function fit(animate=true){"
129
126
  if html.count(signature) != 1:
@@ -148,7 +145,7 @@ def _preserve_readable_initial_camera(html: str) -> str:
148
145
  raise RuntimeError("initial camera fit seams changed")
149
146
  return html.replace(
150
147
  initial_fit,
151
- "if(!hasFitted&&positions.size){fit(false,.5);hasFitted=true}",
148
+ "if(!hasFitted&&positions.size){fit(false,.48);hasFitted=true}",
152
149
  )
153
150
 
154
151
 
@@ -199,7 +196,8 @@ function execweaveFileClusterHistory(a){
199
196
  add('Model',node?.name);
200
197
  add('Provider',a.provider||a.provider_name);
201
198
  add('Inference calls',a.viewer_inference_count);
202
- add('Inference history',execweaveInferenceHistory(a));
199
+ if(a.model_resource_id)add('Shared model resource',a.model_resource_id);
200
+ // Chronology is rendered below as persistent per-invocation accordions.
203
201
  }else if(kind==='tool_call'){"""
204
202
  if html.count(branch_seam) != 1:
205
203
  raise RuntimeError("agent panel file/model branch seam changed")
@@ -224,7 +222,7 @@ def _stop_conversation_polling_after_finish(html: str) -> str:
224
222
  ),
225
223
  (
226
224
  "async function refresh(){if(window.__execweaveStaticMode||refreshing)return;refreshing=true;try{const headers={};if(window.__execweaveToken)headers['X-ExecWeave-Token']=window.__execweaveToken;const response=await fetch('/conversations.json',{cache:'no-store',headers});if(response.ok){const payload=await response.json();setEntries(payload?.entries)}}catch(_){}finally{refreshing=false}}",
227
- "async function refresh({allowDuringFinish=false}={}){if(window.__execweaveStaticMode||conversationPollingFinished||(conversationFinishing&&!allowDuringFinish))return false;if(refreshing)return conversationRefreshPromise;refreshing=true;const controller=new AbortController();conversationRefreshController=controller;const task=(async()=>{try{const headers={};if(window.__execweaveToken)headers['X-ExecWeave-Token']=window.__execweaveToken;const response=await fetch('/conversations.json',{cache:'no-store',headers,signal:controller.signal});if(!response.ok)return false;const payload=await response.json();setEntries(payload?.entries);return true}catch(_){return false}finally{if(conversationRefreshController===controller)conversationRefreshController=null;refreshing=false}})();conversationRefreshPromise=task;return await task}",
225
+ "async function refresh({allowDuringFinish=false}={}){if(window.__execweaveStaticMode||(conversationPollingFinished&&!allowDuringFinish)||(conversationFinishing&&!allowDuringFinish))return false;if(refreshing)return conversationRefreshPromise;refreshing=true;const controller=new AbortController();conversationRefreshController=controller;const timeout=setTimeout(()=>controller.abort(),5000);const task=(async()=>{try{const headers={};if(window.__execweaveToken)headers['X-ExecWeave-Token']=window.__execweaveToken;const response=await fetch('/conversations.json',{cache:'no-store',headers,signal:controller.signal});if(!response.ok)return false;const payload=await response.json();if(!Array.isArray(payload?.entries))return false;setEntries(payload.entries);return true}catch(_){return false}finally{clearTimeout(timeout);if(conversationRefreshController===controller)conversationRefreshController=null;refreshing=false}})();conversationRefreshPromise=task;return await task}",
228
226
  "agent conversation refresh seam changed",
229
227
  ),
230
228
  (
@@ -234,7 +232,7 @@ def _stop_conversation_polling_after_finish(html: str) -> str:
234
232
  ),
235
233
  (
236
234
  "const previous=window.__execweaveDashboard||{};window.__execweaveDashboard={...previous,onPayload(data){previous.onPayload?.(data);if(selectedNode)refresh()},onFinished(){previous.onFinished?.();if(selectedNode)refresh()}};",
237
- "async function finishConversationPolling(){if(conversationPollingFinished)return conversationFinishSynchronized;if(conversationFinishing)return conversationFinishPromise;conversationFinishing=true;if(conversationRefreshTimer!==null){clearInterval(conversationRefreshTimer);conversationRefreshTimer=null}conversationFinishPromise=(async()=>{await conversationRefreshPromise;if(!selectedNode||window.__execweaveStaticMode)return true;return await refresh({allowDuringFinish:true})})().then(synchronized=>{conversationFinishSynchronized=Boolean(synchronized);return conversationFinishSynchronized}).finally(()=>{conversationPollingFinished=true;conversationFinishing=false;if(conversationRefreshController!==null){conversationRefreshController.abort();conversationRefreshController=null}});return await conversationFinishPromise}\nconst stopConversationPolling=finishConversationPolling;\nconst previous=window.__execweaveDashboard||{};window.__execweaveDashboard={...previous,onPayload(data){previous.onPayload?.(data);if(selectedNode&&!data?.live_finished&&!conversationFinishing&&!conversationPollingFinished)refresh()},onFinished(){previous.onFinished?.();void finishConversationPolling()}};",
235
+ "function reportFinalConversationSync(ok){let notice=document.getElementById('conversation-sync-status');if(ok){notice?.remove();return}if(notice)return;notice=document.createElement('div');notice.id='conversation-sync-status';notice.setAttribute('role','status');notice.style.cssText='position:fixed;right:18px;bottom:18px;z-index:100;max-width:360px;padding:12px;background:#2b2118;color:#fff;border:1px solid #e5ad63;border-radius:8px';notice.append(document.createTextNode('Conversation history may be incomplete. Final synchronization failed. '));const retry=document.createElement('button');retry.type='button';retry.textContent='Retry sync';retry.onclick=async()=>{retry.disabled=true;try{await finishConversationPolling()}finally{retry.disabled=false}};notice.append(retry);document.body.append(notice)}\nasync function finishConversationPolling(){if(conversationPollingFinished&&conversationFinishSynchronized)return true;if(conversationFinishing)return conversationFinishPromise;conversationFinishing=true;if(conversationRefreshTimer!==null){clearInterval(conversationRefreshTimer);conversationRefreshTimer=null}conversationFinishPromise=(async()=>{await conversationRefreshPromise;if(window.__execweaveStaticMode)return true;return await refresh({allowDuringFinish:true})})().then(synchronized=>{conversationFinishSynchronized=Boolean(synchronized);reportFinalConversationSync(conversationFinishSynchronized);return conversationFinishSynchronized}).finally(()=>{conversationPollingFinished=true;conversationFinishing=false;if(conversationRefreshController!==null){conversationRefreshController.abort();conversationRefreshController=null}});return await conversationFinishPromise}\nconst stopConversationPolling=finishConversationPolling;\nconst previous=window.__execweaveDashboard||{};window.__execweaveDashboard={...previous,onPayload(data){previous.onPayload?.(data);if(selectedNode&&!data?.live_finished&&!conversationFinishing&&!conversationPollingFinished)refresh()},onFinished(){previous.onFinished?.();void finishConversationPolling()}};",
238
236
  "agent conversation lifecycle seam changed",
239
237
  ),
240
238
  (
@@ -0,0 +1,44 @@
1
+ """Durable export status: session.finished is not proof that files were exported."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ REQUIRED_EXPORTS = ("graph.json", "viewer.html", "conversations.json")
13
+
14
+
15
+ def record_finalization(
16
+ run_dir: Path, *, state: str, error: BaseException | None = None,
17
+ ) -> dict[str, Any]:
18
+ files: dict[str, Any] = {}
19
+ for name in REQUIRED_EXPORTS:
20
+ path = run_dir / name
21
+ if path.is_file():
22
+ digest = hashlib.sha256()
23
+ with path.open("rb") as handle:
24
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
25
+ digest.update(chunk)
26
+ files[name] = {"size_bytes": path.stat().st_size, "sha256": digest.hexdigest()}
27
+ missing = [name for name in REQUIRED_EXPORTS if not files.get(name, {}).get("size_bytes")]
28
+ payload = {"schema_version": "0.1", "state": state, "artifacts": files,
29
+ "missing": missing, "error_type": type(error).__name__ if error else None}
30
+ if state == "complete" and missing:
31
+ payload["state"] = "incomplete"
32
+ fd, temporary = tempfile.mkstemp(prefix=".finalization-", dir=run_dir)
33
+ try:
34
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
35
+ json.dump(payload, handle, indent=2, sort_keys=True)
36
+ handle.write("\n")
37
+ handle.flush()
38
+ os.fsync(handle.fileno())
39
+ os.replace(temporary, run_dir / "finalization.json")
40
+ finally:
41
+ Path(temporary).unlink(missing_ok=True)
42
+ if state == "complete" and missing and error is None:
43
+ raise RuntimeError("Final export is incomplete: " + ", ".join(missing))
44
+ return payload
@@ -7,6 +7,7 @@ from datetime import datetime, timezone
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ from .session_summary import SessionSummary
10
11
  from .fidelity import FidelityAccumulator
11
12
  from .provider_lifecycle import ProviderLifecycleAnnotation, provider_lifecycle_annotation
12
13
  from .validate import validate_event_stream
@@ -346,6 +347,10 @@ class ExecutionGraph:
346
347
  default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
347
348
  )
348
349
 
350
+ evidence_counts: dict[str, int] = field(default_factory=dict)
351
+ session_outcome: dict[str, Any] = field(default_factory=dict)
352
+ runtime_environment: dict[str, Any] = field(default_factory=dict)
353
+
349
354
  def to_dict(self) -> dict[str, Any]:
350
355
  return {
351
356
  "graph_schema_version": GRAPH_SCHEMA_VERSION,
@@ -357,6 +362,9 @@ class ExecutionGraph:
357
362
  "edge_count": len(self.edges),
358
363
  "built_at": self.built_at,
359
364
  "fidelity": self.fidelity,
365
+ "evidence_counts": dict(self.evidence_counts),
366
+ "session_outcome": dict(self.session_outcome),
367
+ "runtime_environment": dict(self.runtime_environment),
360
368
  "nodes": [node.to_dict() for node in self.nodes],
361
369
  "edges": [edge.to_dict() for edge in self.edges],
362
370
  }
@@ -391,6 +399,7 @@ class GraphAccumulator:
391
399
  self.nodes: dict[str, GraphNode] = {}
392
400
  self.edges: dict[tuple[str, str, str], GraphEdge] = {}
393
401
  self._fidelity = FidelityAccumulator()
402
+ self._session_summary = SessionSummary()
394
403
  self._seen_logical_events: set[str] = set()
395
404
 
396
405
  @property
@@ -415,6 +424,7 @@ class GraphAccumulator:
415
424
  self.source_schema_versions.add(schema_version)
416
425
  self.event_count += 1
417
426
  self._fidelity.observe(event)
427
+ self._session_summary.observe(event)
418
428
  logical_key = logical_event_key(event)
419
429
  logical = logical_key not in self._seen_logical_events
420
430
  if logical_key is not None:
@@ -473,6 +483,9 @@ class GraphAccumulator:
473
483
  self.edges.values(), key=lambda edge: (edge.source, edge.relation, edge.target)
474
484
  ),
475
485
  fidelity=self._fidelity.to_dict(),
486
+ evidence_counts=dict(self._session_summary.counts),
487
+ session_outcome=dict(self._session_summary.outcome),
488
+ runtime_environment=dict(self._session_summary.environment),
476
489
  )
477
490
 
478
491
  def to_dict(self) -> dict[str, Any]:
@@ -0,0 +1,16 @@
1
+ """Read JSON hook transport as UTF-8, independently of the terminal code page."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ def read_hook_text(source: Any) -> str:
8
+ binary = getattr(source, "buffer", None)
9
+ raw = binary.read() if binary is not None else source.read()
10
+ if isinstance(raw, bytes):
11
+ # Strict decoding avoids silently replacing evidence. Invalid input is
12
+ # reported by the existing fail-open hook boundary, not mis-recorded.
13
+ return raw.decode("utf-8-sig")
14
+ if not isinstance(raw, str):
15
+ raise ValueError("hook stdin must supply UTF-8 bytes or text")
16
+ return raw.lstrip("\ufeff")
@@ -19,6 +19,8 @@ from urllib.parse import parse_qs, urlencode, urlsplit
19
19
  from uuid import uuid4
20
20
 
21
21
  from .backends import create_collector
22
+ from .conversation_records import write_conversation_records
23
+ from .finalization import record_finalization
22
24
  from .graph import GRAPH_SCHEMA_VERSION, GraphAccumulator, build_execution_graph, write_execution_graph
23
25
  from .live_view import LIVE_HTML as _LIVE_HTML
24
26
  from .semantic import LiveSemanticNormalizer, merge_semantic_sidecar
@@ -152,6 +154,9 @@ def _compact_live_graph(graph: dict[str, object]) -> dict[str, object]:
152
154
  "nodes": [],
153
155
  "edges": [],
154
156
  "live_payload_compact": True,
157
+ "evidence_counts": graph.get("evidence_counts"),
158
+ "session_outcome": graph.get("session_outcome"),
159
+ "runtime_environment": graph.get("runtime_environment"),
155
160
  }
156
161
 
157
162
 
@@ -710,6 +715,7 @@ def run_live(
710
715
  if artifact.exists() and artifact.stat().st_size > 0:
711
716
  raise FileExistsError(f"ExecWeave live artifact already exists: {artifact}")
712
717
 
718
+ record_finalization(run_dir, state="recording")
713
719
  sink = JsonlSink(event_path)
714
720
  collector = create_collector(
715
721
  backend="portable",
@@ -748,9 +754,15 @@ def run_live(
748
754
  key: os.environ.get(key) for key in environment_updates
749
755
  }
750
756
  os.environ.update(environment_updates)
757
+ collection_error: Exception | None = None
758
+ export_complete = False
751
759
  try:
752
760
  try:
753
761
  return_code = collector.run(command)
762
+ except Exception as exc:
763
+ # The collector can have written a valid terminal stream before
764
+ # raising. Export it rather than leaving only raw evidence behind.
765
+ collection_error = exc
754
766
  finally:
755
767
  for key, value in previous_environment.items():
756
768
  if value is None:
@@ -758,6 +770,7 @@ def run_live(
758
770
  else:
759
771
  os.environ[key] = value
760
772
 
773
+ record_finalization(run_dir, state="exporting", error=collection_error)
761
774
  validation = validate_event_stream(event_path)
762
775
  if not validation.valid:
763
776
  details = "; ".join(validation.errors)
@@ -771,11 +784,20 @@ def run_live(
771
784
  execution_graph = build_execution_graph(materialized_event_path)
772
785
  graph_payload = execution_graph.to_dict()
773
786
  write_execution_graph(execution_graph, graph_path)
787
+ # The extracted core must materialize the conversation index itself. The
788
+ # projected live wrapper also writes it, but direct live_core callers (and
789
+ # failure-path export) must not depend on that presentation monkeypatch.
790
+ write_conversation_records(graph_payload, run_dir)
774
791
  write_graph_html(graph_payload, viewer_path, open_browser=False)
792
+ record_finalization(run_dir, state="complete", error=collection_error)
793
+ export_complete = True
775
794
  state.finish(graph_payload, final_html=viewer_path.read_text(encoding="utf-8"))
776
795
  if linger_seconds:
777
796
  time.sleep(linger_seconds)
778
797
 
798
+ if collection_error is not None:
799
+ raise collection_error
800
+
779
801
  return LiveResult(
780
802
  session_id=session_id,
781
803
  return_code=return_code,
@@ -787,6 +809,35 @@ def run_live(
787
809
  graph=graph_path,
788
810
  viewer=viewer_path,
789
811
  )
812
+ except Exception as exc:
813
+ # Export/finalization diagnostics are secondary to an already observed
814
+ # collector failure. Preserve the primary exception and attach later
815
+ # failures as notes rather than replacing the root cause.
816
+ primary_error = collection_error if collection_error is not None else exc
817
+ if collection_error is not None and exc is not collection_error:
818
+ try:
819
+ collection_error.add_note(
820
+ f"secondary export failure: {type(exc).__name__}: {exc}"
821
+ )
822
+ except AttributeError: # pragma: no cover - Python < 3.11 compatibility
823
+ pass
824
+ try:
825
+ record_finalization(
826
+ run_dir,
827
+ state="complete" if export_complete else "failed",
828
+ error=primary_error,
829
+ )
830
+ except Exception as finalization_error:
831
+ try:
832
+ primary_error.add_note(
833
+ "secondary finalization diagnostic failure: "
834
+ f"{type(finalization_error).__name__}: {finalization_error}"
835
+ )
836
+ except AttributeError: # pragma: no cover - Python < 3.11 compatibility
837
+ pass
838
+ if primary_error is not exc:
839
+ raise primary_error from exc
840
+ raise
790
841
  finally:
791
842
  server.shutdown()
792
843
  server.server_close()
@@ -15,7 +15,19 @@ function nodeCategory(node){const value=String(node?.type||'').toLowerCase();if(
15
15
  function graphCounts(data){const nodes=Number(data.node_count)||((data.nodes||[]).length),edges=Number(data.edge_count)||((data.edges||[]).length);return{nodes,edges,estimated:nodes*4+edges*3}}
16
16
  function withinRenderBudget(data){const counts=graphCounts(data);return counts.nodes<=ACTIVE_MAX_NODES&&counts.edges<=ACTIVE_MAX_EDGES&&counts.estimated<=ACTIVE_MAX_DOM_ELEMENTS}
17
17
  function updateStats(data){stats.innerHTML=`<strong>${Number(data.node_count)||0}</strong> nodes · <strong>${Number(data.edge_count)||0}</strong> edges · <strong>${Number(data.event_count)||0}</strong> events`}
18
- function updateEvidence(data){const counts=data.live_evidence_counts||{},runtime=Number(counts.os_runtime)||0,specialized=Number(counts.specialized)||0,provisional=!!data.live_specialized_provisional;evidence.innerHTML=`OS <strong>${runtime}</strong> · specialized <strong>${specialized}</strong>${provisional?' · provisional':''}`;evidence.classList.toggle('provisional',provisional);evidence.title=provisional?'Specialized evidence is provisional until the canonical final merge.':'Observed evidence counts for this live session.'}
18
+ function updateEvidence(data){
19
+ const raw=window.__execweaveCore?.getGraph?.()||window.__execweaveStaticGraph||{},counts=data.live_evidence_counts||data.evidence_counts||raw.evidence_counts||{};
20
+ const count=value=>typeof value==='number'&&Number.isInteger(value)&&value>=0?String(value):'—';
21
+ const provisional=!!data.live_specialized_provisional,specialized=count(counts.specialized);
22
+ const specializedMarkup=`specialized ${specialized}`.replace(specialized,`<strong>${specialized}</strong>`);
23
+ evidence.innerHTML=`OS <strong>${count(counts.os_runtime)}</strong> · ${specializedMarkup}${provisional?' · provisional':''}`;
24
+ evidence.classList.toggle('provisional',provisional);evidence.title='Observed event counts. — means unavailable, not zero.';
25
+ const outcome=data.session_outcome||raw.session_outcome||{},labels={succeeded:'SUCCEEDED',failed:'FAILED',interrupted:'INTERRUPTED',collector_failed:'COLLECTOR FAILED',unknown:'UNKNOWN'};
26
+ let badge=document.getElementById('workload-outcome');if(!badge){badge=document.createElement('span');badge.id='workload-outcome';evidence.after(badge)}
27
+ const finished=outcome.recorder_finished||data.live_finished||window.__execweaveStaticMode;
28
+ badge.textContent=finished?` · Workload: ${labels[outcome.state]||'UNKNOWN'}${Number.isInteger(outcome.return_code)?' · exit '+outcome.return_code:''}`:'';
29
+ badge.dataset.state=outcome.state||'unknown';badge.title='Workload result is separate from recorder FINISHED status.';
30
+ }
19
31
  function setStatus(label,kind=''){statusLabel.textContent=label;status.className=kind}
20
32
  function enterProtectiveMode(data){const counts=graphCounts(data);edgeLayer.replaceChildren();labelLayer.replaceChildren();nodeLayer.replaceChildren();positions=new Map();nodeElements=new Map();edgeElements=new Map();svg.style.display='none';protective.hidden=false;protectiveSummary.textContent=`${counts.nodes} nodes · ${counts.edges} edges · about ${counts.estimated} SVG elements exceeds the live safety budget.`;setStatus('PROTECTED','reconnecting');protectedMode=true}
21
33
  function leaveProtectiveMode(){protective.hidden=true;svg.style.display='block';protectedMode=false}
@@ -0,0 +1,48 @@
1
+ """Portable evidence counts and workload outcome derived from observed events.
2
+
3
+ Recorder completion is deliberately separate from workload success. Missing
4
+ terminal evidence is unknown, never a synthesized successful exit.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+
11
+ class SessionSummary:
12
+ def __init__(self) -> None:
13
+ self.counts = {"os_runtime": 0, "specialized": 0}
14
+ self.outcome: dict[str, Any] = {"state": "unknown", "return_code": None}
15
+ self.environment: dict[str, Any] = {}
16
+
17
+ def observe(self, event: dict[str, Any]) -> None:
18
+ attributes = event.get("attributes")
19
+ attributes = attributes if isinstance(attributes, dict) else {}
20
+ kind = str(event.get("event_type") or "")
21
+ backend = attributes.get("backend")
22
+ semantic = (kind.startswith("semantic.")
23
+ or (isinstance(backend, str) and backend in {"semantic", "model_runtime"})
24
+ or attributes.get("attribution") == "semantic_sidecar")
25
+ self.counts["specialized" if semantic else "os_runtime"] += 1
26
+ if kind == "session.started":
27
+ for key in ("execweave_version", "python_executable", "python_version", "package_path", "backend"):
28
+ value = attributes.get(key)
29
+ if isinstance(value, str):
30
+ self.environment[key] = value
31
+ if kind != "session.finished":
32
+ return
33
+ code = attributes.get("return_code")
34
+ if not isinstance(code, int) or isinstance(code, bool):
35
+ code = None
36
+ interrupted = attributes.get("interrupted") is True
37
+ collector_failed = attributes.get("collector_failed") is True
38
+ state = (
39
+ "collector_failed" if collector_failed else
40
+ "interrupted" if interrupted else
41
+ "succeeded" if code == 0 else
42
+ "failed" if code is not None else "unknown"
43
+ )
44
+ self.outcome = {
45
+ "state": state, "return_code": code, "interrupted": interrupted,
46
+ "collector_failed": collector_failed, "event_id": event.get("event_id"),
47
+ "timestamp": event.get("timestamp"), "recorder_finished": True,
48
+ }