execweave 0.8.20__tar.gz → 0.8.22__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 (179) hide show
  1. {execweave-0.8.20 → execweave-0.8.22}/PKG-INFO +4 -1
  2. {execweave-0.8.20 → execweave-0.8.22}/pyproject.toml +5 -2
  3. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/__init__.py +1 -1
  4. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/_dashboard_shell_base.py +4 -1
  5. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_full_fidelity_collaboration_base.py +7 -1
  6. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_delegation_base.py +5 -0
  7. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_task_linkage.py +10 -1
  8. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_execution_flow.py +222 -45
  9. execweave-0.8.22/src/execweave/viewer_execution_flow_provider_normalization.py +105 -0
  10. execweave-0.8.22/src/execweave/viewer_execution_flow_safety.py +364 -0
  11. execweave-0.8.20/src/execweave/viewer_execution_flow_safety.py +0 -174
  12. {execweave-0.8.20 → execweave-0.8.22}/.gitignore +0 -0
  13. {execweave-0.8.20 → execweave-0.8.22}/LICENSE +0 -0
  14. {execweave-0.8.20 → execweave-0.8.22}/README.md +0 -0
  15. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/__main__.py +0 -0
  16. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/_conversation_records_core.py +0 -0
  17. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/_http_proxy_base.py +0 -0
  18. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/_http_proxy_bounded.py +0 -0
  19. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/_http_proxy_stage.py +0 -0
  20. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/agent_bootstrap.py +0 -0
  21. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/agent_topology.py +0 -0
  22. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/agent_trace.py +0 -0
  23. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/agy_preview_sanitize.py +0 -0
  24. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/analysis.py +0 -0
  25. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/anthropic.py +0 -0
  26. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/anthropic_cli.py +0 -0
  27. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/anthropic_full_fidelity.py +0 -0
  28. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_adapter.py +0 -0
  29. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_adapter_base.py +0 -0
  30. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_full_fidelity.py +0 -0
  31. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_full_fidelity_base.py +0 -0
  32. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_hook_cli.py +0 -0
  33. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_record.py +0 -0
  34. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_subagent_linkage.py +0 -0
  35. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/antigravity_trace_capability.py +0 -0
  36. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/auto_specialized.py +0 -0
  37. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/backends.py +0 -0
  38. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/benchmark.py +0 -0
  39. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_adapter.py +0 -0
  40. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_child_transcript.py +0 -0
  41. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_delegation.py +0 -0
  42. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_full_fidelity.py +0 -0
  43. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_hook_cli.py +0 -0
  44. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_hook_contract.py +0 -0
  45. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_model_observer.py +0 -0
  46. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/claude_record.py +0 -0
  47. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cli.py +0 -0
  48. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_adapter.py +0 -0
  49. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_conversation.py +0 -0
  50. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_full_fidelity.py +0 -0
  51. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_hook_cli.py +0 -0
  52. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_hook_entry.py +0 -0
  53. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_hook_lifecycle.py +0 -0
  54. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_message_diagnostics.py +0 -0
  55. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_message_transport_diagnostics.py +0 -0
  56. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_record.py +0 -0
  57. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_rollout_structures.py +0 -0
  58. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_rollout_trace.py +0 -0
  59. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/codex_rollout_trace_base.py +0 -0
  60. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/collector.py +0 -0
  61. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/command.py +0 -0
  62. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/content_evidence.py +0 -0
  63. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/content_store.py +0 -0
  64. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_archive.py +0 -0
  65. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_message_identity.py +0 -0
  66. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview.py +0 -0
  67. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_antigravity.py +0 -0
  68. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_claude.py +0 -0
  69. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_codex.py +0 -0
  70. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_common.py +0 -0
  71. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_generic.py +0 -0
  72. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_lines.py +0 -0
  73. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_preview_transcript.py +0 -0
  74. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_records.py +0 -0
  75. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_records_antigravity.py +0 -0
  76. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_records_codex.py +0 -0
  77. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_records_common.py +0 -0
  78. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/conversation_records_ollama.py +0 -0
  79. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/correlation.py +0 -0
  80. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_adapter.py +0 -0
  81. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_delegation.py +0 -0
  82. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_full_fidelity.py +0 -0
  83. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_hook_cli.py +0 -0
  84. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_hook_contract.py +0 -0
  85. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/cursor_record.py +0 -0
  86. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/dashboard_shell.py +0 -0
  87. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/entry.py +0 -0
  88. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/evidence_availability.py +0 -0
  89. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/evidence_grade.py +0 -0
  90. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/fidelity.py +0 -0
  91. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/filesystem.py +0 -0
  92. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/focus.py +0 -0
  93. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/graph.py +0 -0
  94. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/graph_ops.py +0 -0
  95. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/http_proxy.py +0 -0
  96. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/http_proxy_cli.py +0 -0
  97. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/inference_gateway.py +0 -0
  98. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/inference_gateway_cli.py +0 -0
  99. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/inference_gateway_full_fidelity.py +0 -0
  100. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/inference_identity.py +0 -0
  101. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/inference_identity_cli.py +0 -0
  102. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/integrity.py +0 -0
  103. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/integrity_cli.py +0 -0
  104. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/litellm_callback.py +0 -0
  105. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/litellm_callback_cli.py +0 -0
  106. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live.py +0 -0
  107. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_core.py +0 -0
  108. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view.py +0 -0
  109. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_extra_style.py +0 -0
  110. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_markup.py +0 -0
  111. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_process_layout.py +0 -0
  112. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_readability.py +0 -0
  113. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_script_a.py +0 -0
  114. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_script_b.py +0 -0
  115. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_script_c.py +0 -0
  116. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_script_d.py +0 -0
  117. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/live_view_style.py +0 -0
  118. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/model_runtime.py +0 -0
  119. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/model_runtime_cli.py +0 -0
  120. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/model_runtime_full_fidelity.py +0 -0
  121. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/openai_compatible.py +0 -0
  122. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/openai_compatible_cli.py +0 -0
  123. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/openai_compatible_full_fidelity.py +0 -0
  124. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_adapter.py +0 -0
  125. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_event_contract.py +0 -0
  126. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_full_fidelity.py +0 -0
  127. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_hook_cli.py +0 -0
  128. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_plugin_cli.py +0 -0
  129. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/opencode_record.py +0 -0
  130. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/overhead_benchmark.py +0 -0
  131. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/provider_capability.py +0 -0
  132. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/provider_lifecycle.py +0 -0
  133. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/provider_record.py +0 -0
  134. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/rule_pack.py +0 -0
  135. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/rule_pack_cli.py +0 -0
  136. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/runtime_endpoint_availability.py +0 -0
  137. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/scalability_benchmark.py +0 -0
  138. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/schema.py +0 -0
  139. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/scope.py +0 -0
  140. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/semantic.py +0 -0
  141. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/sink.py +0 -0
  142. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/strace_backend.py +0 -0
  143. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/stream_assembly.py +0 -0
  144. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/theme.py +0 -0
  145. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/top.py +0 -0
  146. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/top_cli.py +0 -0
  147. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/validate.py +0 -0
  148. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/vendor/dagre.min.js +0 -0
  149. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/view_cli.py +0 -0
  150. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer.py +0 -0
  151. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel.py +0 -0
  152. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
  153. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_claude.py +0 -0
  154. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_codex.py +0 -0
  155. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_cursor.py +0 -0
  156. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_default.py +0 -0
  157. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_ollama.py +0 -0
  158. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_opencode.py +0 -0
  159. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_agent_panel_registry.py +0 -0
  160. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_content_inspector.py +0 -0
  161. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_dashboard_clean.py +0 -0
  162. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_dashboard_focus.py +0 -0
  163. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_dashboard_hardening.py +0 -0
  164. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
  165. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_dashboard_pr70.py +0 -0
  166. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_edge_decross.py +0 -0
  167. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_external_endpoints.py +0 -0
  168. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_gif.py +0 -0
  169. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_layout_geometry.py +0 -0
  170. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_layout_v2.py +0 -0
  171. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_layout_v2_detached.py +0 -0
  172. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_limits.py +0 -0
  173. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_live_layout.py +0 -0
  174. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_projection.py +0 -0
  175. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_projection_base.py +0 -0
  176. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_projection_bridges.py +0 -0
  177. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_semantic_projection.py +0 -0
  178. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/viewer_tool_mirror_identity.py +0 -0
  179. {execweave-0.8.20 → execweave-0.8.22}/src/execweave/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execweave
3
- Version: 0.8.20
3
+ Version: 0.8.22
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
@@ -45,13 +45,16 @@ Provides-Extra: dev
45
45
  Requires-Dist: pillow>=10.0; extra == 'dev'
46
46
  Requires-Dist: pytest>=8.0; extra == 'dev'
47
47
  Requires-Dist: pywinpty<4,>=3.0.5; (platform_system == 'Windows') and extra == 'dev'
48
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
48
49
  Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
50
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
49
51
  Provides-Extra: e2e
50
52
  Requires-Dist: pillow>=10.0; extra == 'e2e'
51
53
  Requires-Dist: playwright>=1.44; extra == 'e2e'
52
54
  Requires-Dist: pywinpty<4,>=3.0.5; (platform_system == 'Windows') and extra == 'e2e'
53
55
  Provides-Extra: release
54
56
  Requires-Dist: build>=1.2; extra == 'release'
57
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'release'
55
58
  Requires-Dist: twine>=6.0; extra == 'release'
56
59
  Description-Content-Type: text/markdown
57
60
 
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execweave"
7
- # ExecWeave v0.8.20 release metadata
8
- version = "0.8.20"
7
+ # ExecWeave v0.8.22 release metadata
8
+ version = "0.8.22"
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"
@@ -44,13 +44,16 @@ dependencies = [
44
44
  [project.optional-dependencies]
45
45
  dev = [
46
46
  "pytest>=8.0",
47
+ "PyYAML>=6.0",
47
48
  "pillow>=10.0",
48
49
  "ruff>=0.16,<0.17",
50
+ "tomli>=2.0; python_version < '3.11'",
49
51
  "pywinpty>=3.0.5,<4; platform_system == 'Windows'",
50
52
  ]
51
53
  release = [
52
54
  "build>=1.2",
53
55
  "twine>=6.0",
56
+ "tomli>=2.0; python_version < '3.11'",
54
57
  ]
55
58
  # Drives the shipped viewers in a real browser. Kept out of dev so an ordinary
56
59
  # development install stays small; CI installs it and requires the checks to run.
@@ -1,3 +1,3 @@
1
1
  """ExecWeave runtime graph package."""
2
2
 
3
- __version__ = "0.8.20"
3
+ __version__ = "0.8.22"
@@ -10,6 +10,7 @@ from .viewer_dashboard_focus import inject_live_dashboard_focus
10
10
  from .viewer_dashboard_pr70 import inject_pr70_dashboard_repairs
11
11
  from .viewer_edge_decross import inject_edge_decross
12
12
  from .viewer_execution_flow import inject_execution_flow
13
+ from .viewer_execution_flow_provider_normalization import normalize_provider_execution_flow
13
14
  from .viewer_execution_flow_safety import harden_execution_flow_projection
14
15
  from .viewer_layout_v2 import inject_layout_v2
15
16
  from .viewer_layout_v2_detached import inject_layout_v2_detached_cluster_policy
@@ -80,7 +81,9 @@ def _build_dashboard_html() -> str:
80
81
  html = inject_layout_v2(html)
81
82
  html = inject_layout_v2_detached_cluster_policy(html)
82
83
  html = inject_edge_decross(html)
83
- html = harden_execution_flow_projection(inject_execution_flow(html))
84
+ html = normalize_provider_execution_flow(
85
+ harden_execution_flow_projection(inject_execution_flow(html))
86
+ )
84
87
  return _align_agent_panel_topology(inject_agent_panel(_guard_compact_live_snapshot(html)))
85
88
 
86
89
 
@@ -164,7 +164,13 @@ def _invoke_subagent_events(
164
164
  relation="REQUESTED_SUBTASK",
165
165
  source=parent,
166
166
  target=subtask,
167
- attributes={"child_identity_exposed": False},
167
+ attributes={
168
+ "child_identity_exposed": False,
169
+ # Exact requester -> provider conversation/step/subagent-index spec.
170
+ # Validated ASSIGNED_AGENT_TASK still required before inventing a child.
171
+ "identity_exact": True,
172
+ "identity_method": "antigravity_provider_conversation_step_subagent_index",
173
+ },
168
174
  )
169
175
  )
170
176
  if type_name:
@@ -134,6 +134,11 @@ def cursor_delegation_events(
134
134
  subtask = _subtask(payload, child)
135
135
  visibility = agent_trace_visibility("cursor")
136
136
  exact = {
137
+ # Persist the provider's exact subagent id through GraphEdge.to_dict(), not
138
+ # only in event-specific booleans, so the provider-neutral viewer can make
139
+ # the same fail-closed delegation decision from a materialized graph.
140
+ "identity_exact": True,
141
+ "identity_method": "cursor_provider_subagent_id",
137
142
  "provider_subagent_id_exact": True,
138
143
  "exact_child_agent_linkage": True,
139
144
  **visibility,
@@ -172,6 +172,15 @@ def opencode_task_session_events(
172
172
  if isinstance(background, bool):
173
173
  attributes["background_requested"] = background
174
174
 
175
+ # GraphEdge deliberately persists identity_exact / identity_method while the
176
+ # provider-specific booleans above remain event-level evidence. Scope those
177
+ # generic identity claims to the task -> child assignment itself; task content
178
+ # edges should not inherit an unrelated identity method.
179
+ assignment_attributes = {
180
+ **attributes,
181
+ "identity_exact": True,
182
+ "identity_method": "opencode_task_tool_parent_and_child_session_ids",
183
+ }
175
184
  child = _agent(child_session, agent_name=agent_name, parent_session_id=parent_session)
176
185
  events: list[dict[str, Any]] = [
177
186
  {
@@ -180,7 +189,7 @@ def opencode_task_session_events(
180
189
  "relation": "ASSIGNED_AGENT_TASK",
181
190
  "source": _tool_call(current_session, call_id),
182
191
  "target": child,
183
- "attributes": attributes,
192
+ "attributes": assignment_attributes,
184
193
  }
185
194
  ]
186
195
 
@@ -43,17 +43,33 @@ EXECUTION_FLOW_SCRIPT = r"""
43
43
  return ACTION_ALIASES.get(text)||null;
44
44
  };
45
45
  const moment=item=>{
46
- if(Number.isInteger(item?.first_sequence))return{kind:0,value:item.first_sequence};
47
46
  const attrs=attrsOf(item);
48
- if(Number.isInteger(attrs.first_sequence))return{kind:0,value:attrs.first_sequence};
49
- const stamp=item?.first_seen||attrs.first_seen||attrs.timestamp;
50
- if(stamp)return{kind:1,value:String(stamp)};
51
- return{kind:2,value:String(item?.id||'')};
47
+ const sequence=Number.isInteger(item?.first_sequence)?item.first_sequence
48
+ :(Number.isInteger(attrs.first_sequence)?attrs.first_sequence:null);
49
+ const stamp=item?.first_seen||attrs.first_seen||attrs.timestamp||null;
50
+ return{
51
+ sequence:sequence,
52
+ stamp:stamp?String(stamp):null,
53
+ id:String(item?.id||''),
54
+ };
52
55
  };
53
- const momentLE=(left,right)=>{
56
+ const momentComparable=(left,right)=>{
54
57
  if(!left||!right)return false;
55
- if(left.kind!==right.kind)return left.kind<right.kind;
56
- return left.value<=right.value;
58
+ if(left.sequence!==null&&right.sequence!==null)return true;
59
+ if(left.stamp&&right.stamp)return true;
60
+ return false;
61
+ };
62
+ const momentLE=(left,right)=>{
63
+ // Only compare when both ends share a sequence or a timestamp. Mixing kinds
64
+ // (or comparing id fallbacks) falsely orders early actions against future switches.
65
+ if(!momentComparable(left,right))return false;
66
+ if(left.sequence!==null&&right.sequence!==null)return left.sequence<=right.sequence;
67
+ return String(left.stamp)<=String(right.stamp);
68
+ };
69
+ const momentEqual=(left,right)=>{
70
+ if(!momentComparable(left,right))return false;
71
+ if(left.sequence!==null&&right.sequence!==null)return left.sequence===right.sequence;
72
+ return String(left.stamp)===String(right.stamp);
57
73
  };
58
74
  const provider=node=>String(attrsOf(node).provider||'unknown').toLowerCase();
59
75
 
@@ -73,30 +89,58 @@ EXECUTION_FLOW_SCRIPT = r"""
73
89
  incoming.get(edge.target).push(edge);outgoing.get(edge.source).push(edge);
74
90
  }
75
91
  const aliasToAgent=new Map();
92
+ const rememberAlias=(alias,id)=>{
93
+ if(alias===undefined||alias===null)return;
94
+ const text=String(alias).trim();
95
+ if(!text)return;
96
+ let bucket=aliasToAgent.get(text);
97
+ if(!bucket){bucket=new Set();aliasToAgent.set(text,bucket)}
98
+ bucket.add(id);
99
+ };
76
100
  for(const [id,node] of agents){
77
101
  const attrs=attrsOf(node);
78
- const aliases=[id,node.name,attrs.agent_id,attrs.thread_id,attrs.agent_path,attrs.child_agent_path,attrs.nickname];
79
- for(const value of aliases)if(value!==undefined&&value!==null&&String(value).trim())aliasToAgent.set(String(value),id);
102
+ rememberAlias(id,id);
103
+ for(const value of [node.name,attrs.agent_id,attrs.thread_id,attrs.agent_path,attrs.child_agent_path,attrs.nickname]){
104
+ rememberAlias(value,id);
105
+ }
80
106
  }
81
107
  const resolveAgent=value=>{
82
108
  if(value===undefined||value===null)return null;
83
109
  const text=String(value);
110
+ // Exact canonical id always wins over alias lookup.
84
111
  if(agents.has(text))return text;
85
- return aliasToAgent.get(text)||null;
112
+ const bucket=aliasToAgent.get(text);
113
+ if(!bucket||!bucket.size)return null;
114
+ if(bucket.size===1)return [...bucket][0];
115
+ // Non-unique alias: abstain rather than last-writer-wins.
116
+ return null;
117
+ };
118
+ const uniqueOwner=candidates=>{
119
+ const ids=[...new Set((candidates||[]).filter(Boolean))];
120
+ return ids.length===1?ids[0]:null;
86
121
  };
87
122
  const ownerForNode=node=>{
88
123
  if(!node?.id)return null;
89
- for(const edge of incoming.get(node.id)||[])if(agents.has(edge.source))return edge.source;
124
+ const direct=[];
125
+ for(const edge of incoming.get(node.id)||[])if(agents.has(edge.source))direct.push(edge.source);
126
+ const directOwner=uniqueOwner(direct);
127
+ if(directOwner)return directOwner;
128
+ if(direct.length)return null; // ambiguous direct owners
129
+ const viaTurn=[];
90
130
  for(const edge of incoming.get(node.id)||[]){
91
131
  const parent=rawById.get(edge.source);
92
132
  if(parent?.type!=='agent_turn')continue;
93
- for(const grand of incoming.get(parent.id)||[])if(agents.has(grand.source))return grand.source;
133
+ for(const grand of incoming.get(parent.id)||[])if(agents.has(grand.source))viaTurn.push(grand.source);
94
134
  }
135
+ const turnOwner=uniqueOwner(viaTurn);
136
+ if(turnOwner)return turnOwner;
137
+ if(viaTurn.length)return null;
95
138
  const attrs=attrsOf(node);
139
+ const attrOwners=[];
96
140
  for(const key of ['owner_agent_id','agent_id','source_agent_id','thread_id','agent_path']){
97
- const found=resolveAgent(attrs[key]);if(found)return found;
141
+ const found=resolveAgent(attrs[key]);if(found)attrOwners.push(found);
98
142
  }
99
- return null;
143
+ return uniqueOwner(attrOwners);
100
144
  };
101
145
  const agentForAnchor=id=>{
102
146
  if(agents.has(id))return id;
@@ -106,13 +150,23 @@ EXECUTION_FLOW_SCRIPT = r"""
106
150
  };
107
151
 
108
152
  const modelNameIndex=new Map();
153
+ const rememberModelAlias=(alias,id)=>{
154
+ if(alias===undefined||alias===null)return;
155
+ const text=String(alias).trim().toLowerCase();
156
+ if(!text)return;
157
+ let bucket=modelNameIndex.get(text);
158
+ if(!bucket){bucket=new Set();modelNameIndex.set(text,bucket)}
159
+ bucket.add(id);
160
+ };
109
161
  for(const [id,node] of models){
110
162
  const attrs=attrsOf(node);
111
- for(const value of [id,node.name,attrs.model,attrs.model_name,attrs.native_name]){
112
- if(value!==undefined&&value!==null&&String(value).trim())modelNameIndex.set(String(value).toLowerCase(),id);
163
+ rememberModelAlias(id,id);
164
+ for(const value of [node.name,attrs.model,attrs.model_name,attrs.native_name]){
165
+ rememberModelAlias(value,id);
113
166
  }
114
167
  }
115
168
  const modelHintFor=node=>{
169
+ if(!node)return null;
116
170
  const attrs=attrsOf(node);
117
171
  for(const key of ['model','model_name','codex_model','default_model','provider_model']){
118
172
  const value=attrs[key];
@@ -123,8 +177,13 @@ EXECUTION_FLOW_SCRIPT = r"""
123
177
  const modelIdFromHint=hint=>{
124
178
  if(!hint)return null;
125
179
  const lower=String(hint).toLowerCase();
126
- if(modelNameIndex.has(lower))return modelNameIndex.get(lower);
127
- for(const [key,id] of modelNameIndex)if(key.endsWith(lower)||lower.endsWith(key))return id;
180
+ // Exact canonical id first.
181
+ if(models.has(hint))return hint;
182
+ if(models.has(lower))return lower;
183
+ const bucket=modelNameIndex.get(lower);
184
+ if(!bucket||!bucket.size)return null;
185
+ if(bucket.size===1)return [...bucket][0];
186
+ // Ambiguous model name across resources: abstain (no suffix fuzzy match).
128
187
  return null;
129
188
  };
130
189
 
@@ -147,20 +206,43 @@ EXECUTION_FLOW_SCRIPT = r"""
147
206
  if(modelId)addModelEvent(owner,modelId,node);
148
207
  }
149
208
  for(const list of modelEventsByAgent.values())list.sort((a,b)=>{
150
- if(a.moment.kind!==b.moment.kind)return a.moment.kind-b.moment.kind;
151
- return a.moment.value<b.moment.value?-1:a.moment.value>b.moment.value?1:0;
209
+ // Prefer sequence when both have it; otherwise timestamp; never cross-rank kinds.
210
+ if(a.moment.sequence!==null&&b.moment.sequence!==null){
211
+ return a.moment.sequence-b.moment.sequence;
212
+ }
213
+ if(a.moment.stamp&&b.moment.stamp){
214
+ return String(a.moment.stamp)<String(b.moment.stamp)?-1:String(a.moment.stamp)>String(b.moment.stamp)?1:0;
215
+ }
216
+ if(a.moment.sequence!==null&&b.moment.sequence===null)return -1;
217
+ if(a.moment.sequence===null&&b.moment.sequence!==null)return 1;
218
+ return String(a.moment.id).localeCompare(String(b.moment.id));
152
219
  });
153
220
 
154
221
  const resolveModel=(owner,item)=>{
155
- const hinted=modelIdFromHint(modelHintFor(item));
222
+ // Item hint is only for the owner/actor side. Callers that resolve a child
223
+ // must pass null as item so an actor tool-call model cannot pollute lookup.
224
+ const hinted=item?modelIdFromHint(modelHintFor(item)):null;
156
225
  if(hinted)return hinted;
157
226
  const list=modelEventsByAgent.get(owner)||[];
158
227
  if(!list.length)return null;
159
- const at=moment(item);
160
- let chosen=null;
161
- for(const event of list)if(momentLE(event.moment,at))chosen=event.modelId;
162
- if(chosen)return chosen;
163
- const unique=[...new Set(list.map(event=>event.modelId))];
228
+ const at=moment(item||{});
229
+ const eligible=[];
230
+ for(const event of list){
231
+ if(!momentComparable(event.moment,at))continue;
232
+ if(momentLE(event.moment,at))eligible.push(event);
233
+ }
234
+ if(!eligible.length){
235
+ const unique=[...new Set(list.map(event=>event.modelId))];
236
+ return unique.length===1?unique[0]:null;
237
+ }
238
+ // Prefer the latest comparable event; if several tie at the same moment with
239
+ // conflicting models, abstain instead of taking array order.
240
+ let bestMoment=eligible[0].moment;
241
+ for(const event of eligible){
242
+ if(momentLE(bestMoment,event.moment)&&!momentEqual(bestMoment,event.moment))bestMoment=event.moment;
243
+ }
244
+ const tied=eligible.filter(event=>momentEqual(event.moment,bestMoment));
245
+ const unique=[...new Set(tied.map(event=>event.modelId))];
164
246
  return unique.length===1?unique[0]:null;
165
247
  };
166
248
 
@@ -180,18 +262,36 @@ EXECUTION_FLOW_SCRIPT = r"""
180
262
  if(!kind)continue;
181
263
  const sources=(incoming.get(node.id)||[]).filter(edge=>relation(edge)==='STARTED_AGENT_INTERACTION');
182
264
  const targets=(outgoing.get(node.id)||[]).filter(edge=>relation(edge)==='TARGETED_BY_AGENT_INTERACTION');
183
- const owner=sources.map(edge=>agentForAnchor(edge.source)).find(Boolean);
184
- const peerIds=targets.map(edge=>agentForAnchor(edge.target)).filter(Boolean);
185
- if(owner)pushOccurrence(kind,owner,peerIds,node,[node.id],[...sources,...targets].map(edge=>edge.id).filter(Boolean));
265
+ const ownerCandidates=sources.map(edge=>agentForAnchor(edge.source));
266
+ const owner=uniqueOwner(ownerCandidates);
267
+ if(!owner)continue;
268
+ // Unknown endpoints keep the occurrence target-less rather than inventing uniqueness.
269
+ const peerIds=[];
270
+ let peersComplete=true;
271
+ for(const edge of targets){
272
+ const peer=agentForAnchor(edge.target);
273
+ if(!peer){peersComplete=false;continue}
274
+ peerIds.push(peer);
275
+ }
276
+ if(targets.length&&!peersComplete&&!peerIds.length)continue;
277
+ pushOccurrence(kind,owner,peerIds,node,[node.id],[...sources,...targets].map(edge=>edge.id).filter(Boolean));
186
278
  }
187
279
 
188
280
  for(const node of rawNodes){
189
281
  if(node?.type!=='agent_message'||!node.id)continue;
190
282
  const sent=(incoming.get(node.id)||[]).filter(edge=>relation(edge)==='SENT_AGENT_MESSAGE');
191
283
  const delivered=(outgoing.get(node.id)||[]).filter(edge=>relation(edge)==='DELIVERED_AGENT_MESSAGE');
192
- const owner=sent.map(edge=>agentForAnchor(edge.source)).find(Boolean);
193
- const peerIds=delivered.map(edge=>agentForAnchor(edge.target)).filter(Boolean);
194
- if(owner)pushOccurrence('send_input',owner,peerIds,node,[node.id],[...sent,...delivered].map(edge=>edge.id).filter(Boolean));
284
+ const owner=uniqueOwner(sent.map(edge=>agentForAnchor(edge.source)));
285
+ if(!owner)continue;
286
+ const peerIds=[];
287
+ let peersComplete=true;
288
+ for(const edge of delivered){
289
+ const peer=agentForAnchor(edge.target);
290
+ if(!peer){peersComplete=false;continue}
291
+ peerIds.push(peer);
292
+ }
293
+ if(delivered.length&&!peersComplete&&!peerIds.length)continue;
294
+ pushOccurrence('send_input',owner,peerIds,node,[node.id],[...sent,...delivered].map(edge=>edge.id).filter(Boolean));
195
295
  }
196
296
 
197
297
  for(const edge of rawEdges){
@@ -293,13 +393,70 @@ EXECUTION_FLOW_SCRIPT = r"""
293
393
  for(const modelId of new Set(list.map(item=>item.modelId)))ensureContext(owner,modelId);
294
394
  }
295
395
 
296
- const consumedRawNodes=new Set(),consumedRawEdges=new Set(),actionToolIds=new Set();
396
+ const consumedRawNodes=new Set(),consumedRawEdges=new Set(),consumedCallIds=new Set();
397
+ const contentRefFromNode=(contentNode,edge,direction)=>{
398
+ const attrs=attrsOf(contentNode);
399
+ const viewer=attrs.viewer_content&&typeof attrs.viewer_content==='object'?attrs.viewer_content:{};
400
+ return{
401
+ content_node_id:contentNode.id,
402
+ raw_node_id:contentNode.id,
403
+ raw_edge_id:edge?.id||null,
404
+ relation:edge?.relation||null,
405
+ direction:direction||null,
406
+ sha256:viewer.sha256||attrs.sha256||attrs.content_sha256||null,
407
+ path:viewer.safe_relative_path||attrs.path||attrs.content_path||null,
408
+ size_bytes:viewer.size_bytes??attrs.size_bytes??attrs.content_size_bytes??null,
409
+ media_type:viewer.media_type||attrs.media_type||attrs.content_media_type||null,
410
+ content_kind:viewer.content_kind||attrs.content_kind||contentNode.name||null,
411
+ };
412
+ };
297
413
  for(const group of groups.values()){
298
414
  const context=group.modelId?ensureContext(group.owner,group.modelId):null;
299
415
  const source=context||group.owner;
300
416
  const baseRank=agentRank.get(group.owner)||0;
301
417
  const actionRank=baseRank+(context?2:1);
302
418
  const id=`viewer:orchestration:${encodeURIComponent(group.owner)}:${encodeURIComponent(group.modelId||'unresolved')}:${group.kind}`;
419
+ const contentRefs=[];
420
+ const toolCallOccurrences=[];
421
+ for(const rawId of group.rawNodeIds){
422
+ const rawNode=rawById.get(rawId);
423
+ if(!rawNode)continue;
424
+ if(String(rawNode.type||'')==='observed_content'){
425
+ contentRefs.push(contentRefFromNode(rawNode,null,'node'));
426
+ continue;
427
+ }
428
+ for(const edge of [...(outgoing.get(rawId)||[]),...(incoming.get(rawId)||[])]){
429
+ const peerId=edge.source===rawId?edge.target:edge.source;
430
+ const peer=rawById.get(peerId);
431
+ if(peer?.type!=='observed_content')continue;
432
+ const direction=edge.source===rawId?'outgoing':'incoming';
433
+ const ref=contentRefFromNode(peer,edge,direction);
434
+ if(!contentRefs.some(item=>item.content_node_id===ref.content_node_id&&item.raw_edge_id===ref.raw_edge_id)){
435
+ contentRefs.push(ref);
436
+ }
437
+ }
438
+ if(['tool_call','tool_call_observation'].includes(String(rawNode?.type||''))){
439
+ consumedCallIds.add(rawId);
440
+ const attrs=attrsOf(rawNode);
441
+ let toolId=null;
442
+ for(const edge of outgoing.get(rawId)||[]){
443
+ if(relation(edge)==='USES_TOOL'||relation(edge)==='RESOLVED_TOOL'){toolId=edge.target;break}
444
+ }
445
+ toolCallOccurrences.push({
446
+ invocation_id:rawId,
447
+ owner_id:group.owner,
448
+ tool_id:toolId,
449
+ call_ids:[rawId],
450
+ first_sequence:Number.isInteger(rawNode.first_sequence)?rawNode.first_sequence:null,
451
+ last_sequence:Number.isInteger(rawNode.last_sequence)?rawNode.last_sequence:null,
452
+ first_seen:rawNode.first_seen||null,
453
+ last_seen:rawNode.last_seen||null,
454
+ input:attrs.arguments??attrs.args??attrs.tool_input??attrs.input??attrs.parameters??attrs.request??null,
455
+ output:attrs.output??attrs.result??attrs.tool_output??attrs.response??null,
456
+ content_references:contentRefs.filter(ref=>ref.raw_edge_id&&[...(outgoing.get(rawId)||[]),...(incoming.get(rawId)||[])].some(edge=>edge.id===ref.raw_edge_id)),
457
+ });
458
+ }
459
+ }
303
460
  addedNodes.push({
304
461
  id,type:'tool',name:group.kind,
305
462
  attributes:{
@@ -309,6 +466,8 @@ EXECUTION_FLOW_SCRIPT = r"""
309
466
  evidence_node_ids:[...group.rawNodeIds].sort(),evidence_edge_ids:[...group.rawEdgeIds].sort(),
310
467
  viewer_occurrence_count:group.occurrences.length,
311
468
  viewer_model_context_unresolved:!context||undefined,
469
+ viewer_content_references:contentRefs,
470
+ viewer_tool_call_occurrences:toolCallOccurrences,
312
471
  }
313
472
  });
314
473
  addedEdges.push({
@@ -326,13 +485,23 @@ EXECUTION_FLOW_SCRIPT = r"""
326
485
  }
327
486
  for(const rawId of group.rawNodeIds)consumedRawNodes.add(rawId);
328
487
  for(const rawId of group.rawEdgeIds)consumedRawEdges.add(rawId);
329
- for(const rawId of group.rawNodeIds){
330
- const rawNode=rawById.get(rawId);
331
- if(!['tool_call','tool_call_observation'].includes(String(rawNode?.type||'')))continue;
332
- for(const edge of outgoing.get(rawId)||[]){
333
- if(relation(edge)==='USES_TOOL')actionToolIds.add(edge.target);
334
- }
335
- }
488
+ }
489
+
490
+ // Shared tool definitions / model resources stay unless every remaining USES_TOOL
491
+ // into them came from consumed calls. Consuming one owner's call must not wipe the
492
+ // shared definition for other owners or unresolved calls.
493
+ const hideToolDefs=new Set();
494
+ for(const [toolId,node] of [...rawById].filter(([,n])=>n?.type==='tool')){
495
+ const uses=(incoming.get(toolId)||[]).filter(edge=>['USES_TOOL','RESOLVED_TOOL'].includes(relation(edge)));
496
+ if(!uses.length)continue;
497
+ // Keep shared tool definitions when any live use remains: another owner's
498
+ // unconsumed call, an agent-direct USES_TOOL, or unresolved evidence.
499
+ const allConsumedCalls=uses.every(edge=>{
500
+ const src=rawById.get(edge.source);
501
+ const isCall=['tool_call','tool_call_observation'].includes(String(src?.type||''));
502
+ return isCall&&consumedCallIds.has(edge.source);
503
+ });
504
+ if(allConsumedCalls)hideToolDefs.add(toolId);
336
505
  }
337
506
 
338
507
  const flowNodes=display.nodes.map(node=>{
@@ -342,17 +511,25 @@ EXECUTION_FLOW_SCRIPT = r"""
342
511
 
343
512
  const modelRelation=edge=>MODEL_RELATIONS.has(relation(edge))||relation(edge)==='MODEL_CONTEXT';
344
513
  const orchestrationRelation=edge=>DIRECT_ACTION_RELATIONS.has(relation(edge));
345
- let nodes=flowNodes.filter(node=>!consumedRawNodes.has(node.id)&&!actionToolIds.has(node.id));
514
+ let nodes=flowNodes.filter(node=>!consumedRawNodes.has(node.id)&&!hideToolDefs.has(node.id));
346
515
  let edges=display.edges.filter(edge=>{
347
516
  if(consumedRawEdges.has(edge.id))return false;
348
- if(orchestrationRelation(edge)&&agents.has(edge.source)&&agents.has(edge.target))return false;
349
- if(actionToolIds.has(edge.source)||actionToolIds.has(edge.target))return false;
350
- if(contextifiedModels.has(edge.target)&&agents.has(edge.source)&&modelRelation(edge))return false;
517
+ // Keep unconsumed inferred / provider orchestration evidence. Only hide edges
518
+ // that were actually folded into an occurrence (tracked via consumedRawEdges).
519
+ if(orchestrationRelation(edge)&&agents.has(edge.source)&&agents.has(edge.target)){
520
+ return !consumedRawEdges.has(edge.id);
521
+ }
522
+ if(hideToolDefs.has(edge.source)||hideToolDefs.has(edge.target))return false;
523
+ // Only supersede an owner→model edge when THAT owner already has a viewer
524
+ // model-context for the same model. Other owners' shared-model usage stays.
525
+ if(modelRelation(edge)&&agents.has(edge.source)&&contextIds.has(contextId(edge.source,edge.target)))return false;
351
526
  return true;
352
527
  });
353
528
 
354
529
  for(const modelId of contextifiedModels){
355
530
  const remaining=edges.filter(edge=>edge.source===modelId||edge.target===modelId);
531
+ // Keep the raw model resource whenever any non-superseded usage remains
532
+ // (another owner, unresolved call, or non-agent attachment).
356
533
  if(!remaining.length)nodes=nodes.filter(node=>node.id!==modelId);
357
534
  }
358
535
 
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ _EXACT_SUBTASK_ACTION = """ const evidenceEdges=[...requests,...assignments,...childSessionEdges];
5
+ pushOccurrence(
6
+ 'assign_agent_task',owner,[target],assignmentSeed,[subtask.id],
7
+ evidenceEdges.map(edge=>edge.id).filter(Boolean)
8
+ );"""
9
+ _EXACT_SUBTASK_ACTION_NORMALIZED = """ const evidenceEdges=[...requests,...assignments,...childSessionEdges];
10
+ const matchingSpawn=occurrences.find(occurrence=>
11
+ occurrence.kind==='spawn_agent'&&occurrence.owner===owner&&occurrence.targets.has(target)
12
+ );
13
+ if(matchingSpawn){
14
+ if(!matchingSpawn.rawNodeIds.includes(subtask.id))matchingSpawn.rawNodeIds.push(subtask.id);
15
+ for(const edgeIdValue of evidenceEdges.map(edge=>edge.id).filter(Boolean)){
16
+ if(!matchingSpawn.rawEdgeIds.includes(edgeIdValue))matchingSpawn.rawEdgeIds.push(edgeIdValue);
17
+ }
18
+ }else{
19
+ pushOccurrence(
20
+ 'assign_agent_task',owner,[target],assignmentSeed,[subtask.id],
21
+ evidenceEdges.map(edge=>edge.id).filter(Boolean)
22
+ );
23
+ }"""
24
+
25
+ _ACTION_TARGET_END = """ for(const target of [...group.targets].sort()){
26
+ addedEdges.push({
27
+ id:`viewer:${id}--${targetRelation}-->${target}`,source:id,target,relation:targetRelation,
28
+ count:1,viewer_only:true,causal:false,inferred:false,
29
+ attributes:{viewer_only:true,projection:'execution_flow',evidence_node_ids:[...group.rawNodeIds],evidence_edge_ids:[...group.rawEdgeIds]}
30
+ });
31
+ }
32
+ for(const rawId of group.rawNodeIds)consumedRawNodes.add(rawId);"""
33
+ _ACTION_TARGET_WITH_PAYLOADS = """ for(const target of [...group.targets].sort()){
34
+ addedEdges.push({
35
+ id:`viewer:${id}--${targetRelation}-->${target}`,source:id,target,relation:targetRelation,
36
+ count:1,viewer_only:true,causal:false,inferred:false,
37
+ attributes:{viewer_only:true,projection:'execution_flow',evidence_node_ids:[...group.rawNodeIds],evidence_edge_ids:[...group.rawEdgeIds]}
38
+ });
39
+ }
40
+ // Retarget visible detail edges onto the replacement action, but never onto
41
+ // peers that other flow groups also consume (prevents dangling action ->
42
+ // consumed interaction anchors in Codex reduced-state replay).
43
+ const peerConsumedRawNodeIds=new Set();
44
+ for(const other of groups.values())for(const rawId of other.rawNodeIds)peerConsumedRawNodeIds.add(rawId);
45
+ const detailRelations=new Set([
46
+ 'HAS_SUBTASK_PROMPT','HAS_SUBTASK_DESCRIPTION','TARGETS_AGENT_PROFILE',
47
+ 'HAS_COMMAND','HAS_WORKING_DIRECTORY','USES_PROFILE','READS_FILE','WRITES_FILE',
48
+ 'HAS_FILE','OBSERVED_CONTENT','HAS_TOOL_INPUT','HAS_TOOL_OUTPUT'
49
+ ]);
50
+ for(const rawId of group.rawNodeIds){
51
+ for(const rawEdge of outgoing.get(rawId)||[]){
52
+ const payloadRelation=relation(rawEdge);
53
+ if(!detailRelations.has(payloadRelation))continue;
54
+ const detailTarget=rawEdge.target;
55
+ // Skip retargeting into another group's consumed anchors.
56
+ if(peerConsumedRawNodeIds.has(detailTarget)&&!group.rawNodeIds.has(detailTarget))continue;
57
+ if(!display.nodes.some(node=>node?.id===detailTarget)&&!rawById.has(detailTarget))continue;
58
+ // Do not paint observed_content back onto the canvas; content stays hidden
59
+ // and is exposed via viewer_content_references on the action instead.
60
+ const targetNode=rawById.get(detailTarget)||display.nodes.find(node=>node?.id===detailTarget);
61
+ if(targetNode?.type==='observed_content')continue;
62
+ if(targetNode?.type==='agent_interaction')continue;
63
+ addedEdges.push({
64
+ id:`viewer:${id}--${payloadRelation}-->${detailTarget}:evidence:${encodeURIComponent(rawEdge.id||rawId)}`,
65
+ source:id,target:detailTarget,relation:rawEdge.relation||payloadRelation,
66
+ count:Number.isFinite(rawEdge.count)?rawEdge.count:1,viewer_only:true,causal:false,inferred:false,
67
+ attributes:{viewer_only:true,projection:'execution_flow',evidence_node_ids:[rawId],evidence_edge_ids:[rawEdge.id].filter(Boolean)}
68
+ });
69
+ if(rawEdge.id)consumedRawEdges.add(rawEdge.id);
70
+ }
71
+ }
72
+ for(const rawId of group.rawNodeIds)consumedRawNodes.add(rawId);"""
73
+
74
+ _EDGE_FILTER = " if(consumedRawEdges.has(edge.id))return false;"
75
+ _EDGE_FILTER_NO_DANGLING = """ if(consumedRawEdges.has(edge.id))return false;
76
+ // Drop edges that still point at consumed presentation nodes, but never use
77
+ // this as a blanket cover for lost evidence — detail retargeting above must
78
+ // have preserved reachable payloads onto the replacement action first.
79
+ if(consumedRawNodes.has(edge.source)||consumedRawNodes.has(edge.target))return false;"""
80
+
81
+
82
+ def normalize_provider_execution_flow(html: str) -> str:
83
+ """Normalize equivalent exact delegation encodings without weakening evidence gates.
84
+
85
+ Cursor may report both a direct child-start lifecycle edge and an exact subtask
86
+ assignment for the same child. Those are two observations of one delegation, not two
87
+ orchestration actions. The exact subtask evidence is folded into the existing spawn
88
+ occurrence when both owner and child already match. Providers without a direct spawn
89
+ (for example the validated Antigravity assignment path) keep the assignment action.
90
+
91
+ Superseded task/tool-call nodes are presentation-only removals. Any provider-observed
92
+ task prompt/description remains reachable from the replacement orchestration action,
93
+ and no display edge is allowed to reference a consumed node. The raw graph is never
94
+ mutated by this normalizer.
95
+ """
96
+ seams = (
97
+ (_EXACT_SUBTASK_ACTION, _EXACT_SUBTASK_ACTION_NORMALIZED, "exact-subtask fold"),
98
+ (_ACTION_TARGET_END, _ACTION_TARGET_WITH_PAYLOADS, "delegation payload preservation"),
99
+ (_EDGE_FILTER, _EDGE_FILTER_NO_DANGLING, "consumed-node edge filter"),
100
+ )
101
+ for needle, replacement, label in seams:
102
+ if html.count(needle) != 1:
103
+ raise RuntimeError(f"execution-flow provider normalization seam changed: {label}")
104
+ html = html.replace(needle, replacement, 1)
105
+ return html