execweave 0.8.33__tar.gz → 0.8.34__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 (223) hide show
  1. {execweave-0.8.33 → execweave-0.8.34}/PKG-INFO +4 -1
  2. {execweave-0.8.33 → execweave-0.8.34}/pyproject.toml +4 -2
  3. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/__init__.py +1 -1
  4. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_conversation_records_core.py +47 -6
  5. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_dashboard_shell_base.py +16 -1
  6. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_conversation.py +11 -5
  7. execweave-0.8.34/src/execweave/content_integrity.py +289 -0
  8. execweave-0.8.34/src/execweave/controlled_checks.py +561 -0
  9. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_codex.py +1 -1
  10. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records.py +4 -4
  11. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_codex.py +24 -0
  12. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/dashboard_shell.py +16 -7
  13. execweave-0.8.34/src/execweave/delivery_status.py +94 -0
  14. execweave-0.8.34/src/execweave/finalization.py +58 -0
  15. execweave-0.8.34/src/execweave/investigation_index.py +444 -0
  16. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live.py +9 -5
  17. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_core.py +29 -5
  18. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_c.py +16 -1
  19. execweave-0.8.34/src/execweave/redacted_archive.py +906 -0
  20. execweave-0.8.34/src/execweave/run_assessment.py +188 -0
  21. execweave-0.8.34/src/execweave/task_artifacts.py +192 -0
  22. execweave-0.8.34/src/execweave/task_validation.py +317 -0
  23. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel.py +14 -6
  24. execweave-0.8.34/src/execweave/viewer_agent_panel_default.py +79 -0
  25. execweave-0.8.34/src/execweave/viewer_archive_verifier.py +178 -0
  26. execweave-0.8.34/src/execweave/viewer_content_browser.py +307 -0
  27. execweave-0.8.34/src/execweave/viewer_content_health.py +208 -0
  28. execweave-0.8.34/src/execweave/viewer_controlled_checks.py +258 -0
  29. execweave-0.8.34/src/execweave/viewer_conversation_sync.py +115 -0
  30. execweave-0.8.34/src/execweave/viewer_delivery_status.py +155 -0
  31. execweave-0.8.34/src/execweave/viewer_framework_model_output.py +133 -0
  32. execweave-0.8.34/src/execweave/viewer_history_browser.py +225 -0
  33. execweave-0.8.34/src/execweave/viewer_investigation.py +268 -0
  34. execweave-0.8.34/src/execweave/viewer_message_handoffs.py +114 -0
  35. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection.py +19 -4
  36. execweave-0.8.34/src/execweave/viewer_recorded_sources.py +146 -0
  37. execweave-0.8.34/src/execweave/viewer_run_assessment.py +120 -0
  38. execweave-0.8.34/src/execweave/viewer_run_guide.py +175 -0
  39. execweave-0.8.34/src/execweave/viewer_runtime_evidence.py +258 -0
  40. execweave-0.8.34/src/execweave/viewer_structure_share.py +132 -0
  41. execweave-0.8.34/src/execweave/viewer_task_validation.py +241 -0
  42. execweave-0.8.34/src/execweave/viewer_workflow_mode.py +127 -0
  43. execweave-0.8.33/src/execweave/finalization.py +0 -44
  44. execweave-0.8.33/src/execweave/viewer_agent_panel_default.py +0 -36
  45. {execweave-0.8.33 → execweave-0.8.34}/.gitignore +0 -0
  46. {execweave-0.8.33 → execweave-0.8.34}/LICENSE +0 -0
  47. {execweave-0.8.33 → execweave-0.8.34}/README.md +0 -0
  48. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/__main__.py +0 -0
  49. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_base.py +0 -0
  50. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_bounded.py +0 -0
  51. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_stage.py +0 -0
  52. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/acceptance_output.py +0 -0
  53. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_bootstrap.py +0 -0
  54. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_topology.py +0 -0
  55. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_trace.py +0 -0
  56. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agy_preview_sanitize.py +0 -0
  57. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/analysis.py +0 -0
  58. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic.py +0 -0
  59. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic_cli.py +0 -0
  60. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic_full_fidelity.py +0 -0
  61. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_adapter.py +0 -0
  62. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_adapter_base.py +0 -0
  63. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity.py +0 -0
  64. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity_base.py +0 -0
  65. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity_collaboration_base.py +0 -0
  66. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_hook_cli.py +0 -0
  67. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_record.py +0 -0
  68. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_subagent_linkage.py +0 -0
  69. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_trace_capability.py +0 -0
  70. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/auto_specialized.py +0 -0
  71. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/backends.py +0 -0
  72. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/benchmark.py +0 -0
  73. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_adapter.py +0 -0
  74. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_child_transcript.py +0 -0
  75. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_delegation.py +0 -0
  76. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_full_fidelity.py +0 -0
  77. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_hook_cli.py +0 -0
  78. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_hook_contract.py +0 -0
  79. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_model_observer.py +0 -0
  80. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_record.py +0 -0
  81. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cli.py +0 -0
  82. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_adapter.py +0 -0
  83. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_full_fidelity.py +0 -0
  84. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_cli.py +0 -0
  85. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_entry.py +0 -0
  86. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_lifecycle.py +0 -0
  87. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_message_diagnostics.py +0 -0
  88. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_message_transport_diagnostics.py +0 -0
  89. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_record.py +0 -0
  90. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_structures.py +0 -0
  91. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_trace.py +0 -0
  92. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_trace_base.py +0 -0
  93. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/collector.py +0 -0
  94. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/command.py +0 -0
  95. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/content_evidence.py +0 -0
  96. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/content_store.py +0 -0
  97. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_archive.py +0 -0
  98. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_message_identity.py +0 -0
  99. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview.py +0 -0
  100. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_antigravity.py +0 -0
  101. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_claude.py +0 -0
  102. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_common.py +0 -0
  103. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_generic.py +0 -0
  104. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_lines.py +0 -0
  105. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_transcript.py +0 -0
  106. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_antigravity.py +0 -0
  107. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_common.py +0 -0
  108. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_ollama.py +0 -0
  109. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/correlation.py +0 -0
  110. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_adapter.py +0 -0
  111. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_delegation.py +0 -0
  112. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_delegation_base.py +0 -0
  113. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_full_fidelity.py +0 -0
  114. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_hook_cli.py +0 -0
  115. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_hook_contract.py +0 -0
  116. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_lifecycle.py +0 -0
  117. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_record.py +0 -0
  118. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/entry.py +0 -0
  119. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/evidence_availability.py +0 -0
  120. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/evidence_grade.py +0 -0
  121. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/fidelity.py +0 -0
  122. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/filesystem.py +0 -0
  123. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/focus.py +0 -0
  124. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/__init__.py +0 -0
  125. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/autogen.py +0 -0
  126. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/base.py +0 -0
  127. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/camel.py +0 -0
  128. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/camel_channel.py +0 -0
  129. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/metagpt.py +0 -0
  130. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/registry.py +0 -0
  131. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/graph.py +0 -0
  132. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/graph_ops.py +0 -0
  133. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/hook_input.py +0 -0
  134. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/http_proxy.py +0 -0
  135. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/http_proxy_cli.py +0 -0
  136. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway.py +0 -0
  137. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway_cli.py +0 -0
  138. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway_full_fidelity.py +0 -0
  139. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_identity.py +0 -0
  140. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_identity_cli.py +0 -0
  141. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/integrity.py +0 -0
  142. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/integrity_cli.py +0 -0
  143. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/litellm_callback.py +0 -0
  144. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/litellm_callback_cli.py +0 -0
  145. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view.py +0 -0
  146. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_extra_style.py +0 -0
  147. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_markup.py +0 -0
  148. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_process_layout.py +0 -0
  149. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_readability.py +0 -0
  150. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_a.py +0 -0
  151. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_b.py +0 -0
  152. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_d.py +0 -0
  153. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_style.py +0 -0
  154. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime.py +0 -0
  155. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime_cli.py +0 -0
  156. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime_full_fidelity.py +0 -0
  157. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible.py +0 -0
  158. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible_cli.py +0 -0
  159. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible_full_fidelity.py +0 -0
  160. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_adapter.py +0 -0
  161. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_event_contract.py +0 -0
  162. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_full_fidelity.py +0 -0
  163. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_hook_cli.py +0 -0
  164. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_plugin_cli.py +0 -0
  165. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_record.py +0 -0
  166. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_task_linkage.py +0 -0
  167. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/overhead_benchmark.py +0 -0
  168. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_capability.py +0 -0
  169. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_lifecycle.py +0 -0
  170. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_record.py +0 -0
  171. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/rule_pack.py +0 -0
  172. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/rule_pack_cli.py +0 -0
  173. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/runtime_endpoint_availability.py +0 -0
  174. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/scalability_benchmark.py +0 -0
  175. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/schema.py +0 -0
  176. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/scope.py +0 -0
  177. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/semantic.py +0 -0
  178. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/session_summary.py +0 -0
  179. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/sink.py +0 -0
  180. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/strace_backend.py +0 -0
  181. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/stream_assembly.py +0 -0
  182. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/theme.py +0 -0
  183. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/top.py +0 -0
  184. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/top_cli.py +0 -0
  185. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/validate.py +0 -0
  186. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/vendor/dagre.min.js +0 -0
  187. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/view_cli.py +0 -0
  188. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer.py +0 -0
  189. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
  190. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_claude.py +0 -0
  191. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_codex.py +0 -0
  192. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_cursor.py +0 -0
  193. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_ollama.py +0 -0
  194. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_opencode.py +0 -0
  195. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_registry.py +0 -0
  196. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_background_files.py +0 -0
  197. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_content_inspector.py +0 -0
  198. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_clean.py +0 -0
  199. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_focus.py +0 -0
  200. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_hardening.py +0 -0
  201. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
  202. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_pr70.py +0 -0
  203. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_edge_decross.py +0 -0
  204. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow.py +0 -0
  205. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow_provider_normalization.py +0 -0
  206. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow_safety.py +0 -0
  207. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_external_endpoints.py +0 -0
  208. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_flow_canvas.py +0 -0
  209. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_flow_layout.py +0 -0
  210. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_fold_state.py +0 -0
  211. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_framework_flow.py +0 -0
  212. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_gif.py +0 -0
  213. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_geometry.py +0 -0
  214. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_v2.py +0 -0
  215. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_v2_detached.py +0 -0
  216. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_limits.py +0 -0
  217. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_live_layout.py +0 -0
  218. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection_base.py +0 -0
  219. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection_bridges.py +0 -0
  220. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_semantic_projection.py +0 -0
  221. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_session_flow.py +0 -0
  222. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_tool_mirror_identity.py +0 -0
  223. {execweave-0.8.33 → execweave-0.8.34}/src/execweave/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execweave
3
- Version: 0.8.33
3
+ Version: 0.8.34
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
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
42
42
  Requires-Dist: psutil>=5.9
43
43
  Requires-Dist: watchdog>=4.0
44
44
  Provides-Extra: dev
45
+ Requires-Dist: cryptography>=44; extra == 'dev'
45
46
  Requires-Dist: pillow>=10.0; extra == 'dev'
46
47
  Requires-Dist: pytest>=8.0; extra == 'dev'
47
48
  Requires-Dist: pywinpty<4,>=3.0.5; (platform_system == 'Windows') and extra == 'dev'
@@ -56,6 +57,8 @@ Provides-Extra: release
56
57
  Requires-Dist: build>=1.2; extra == 'release'
57
58
  Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'release'
58
59
  Requires-Dist: twine>=6.0; extra == 'release'
60
+ Provides-Extra: validation
61
+ Requires-Dist: cryptography>=44; extra == 'validation'
59
62
  Description-Content-Type: text/markdown
60
63
 
61
64
  # ExecWeave
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execweave"
7
- # ExecWeave v0.8.33 release metadata
8
- version = "0.8.33"
7
+ # ExecWeave v0.8.34 release metadata
8
+ version = "0.8.34"
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"
@@ -42,7 +42,9 @@ dependencies = [
42
42
  ]
43
43
 
44
44
  [project.optional-dependencies]
45
+ validation = ["cryptography>=44"]
45
46
  dev = [
47
+ "cryptography>=44",
46
48
  "pytest>=8.0",
47
49
  "PyYAML>=6.0",
48
50
  "pillow>=10.0",
@@ -1,3 +1,3 @@
1
1
  """ExecWeave runtime graph package."""
2
2
 
3
- __version__ = "0.8.33"
3
+ __version__ = "0.8.34"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
+ from collections import Counter
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
  from .content_store import _filesystem_path
@@ -491,6 +492,7 @@ def conversation_record_entries(
491
492
  for node in nodes
492
493
  if isinstance(node.get("id"), str) and node.get("id")
493
494
  }
495
+ source_counts = Counter(node.get("id") for node in nodes if isinstance(node.get("id"), str))
494
496
  entries: list[dict[str, Any]] = []
495
497
  for edge in edges:
496
498
  target_id = edge.get("target")
@@ -542,6 +544,36 @@ def conversation_record_entries(
542
544
  derived = preview.pop("derived_agent_previews", None) if isinstance(preview, dict) else None
543
545
  if preview is not None:
544
546
  entry["conversation_preview"] = preview
547
+ # Keep each framework model response attached to its own content reference.
548
+ # Conversation merging publishes only one aggregate preview and can otherwise
549
+ # leave the actual model bodies behind a later control/status message.
550
+ source_attrs = source.get("attributes") if isinstance(source, dict) else None
551
+ if (
552
+ isinstance(source_attrs, dict)
553
+ and source.get("type") == "agent"
554
+ and source_counts[source_id] == 1
555
+ and source_attrs.get("conversation_scope") == "framework_agent"
556
+ and reference["content_kind"] in {
557
+ "metagpt.model_response", "autogen.model_response", "camel.model_response"
558
+ }
559
+ and edge.get("relation") == "HAS_MODEL_CONTENT"
560
+ and all(
561
+ record.get(flag) is None or record.get(flag) is False
562
+ for record in (edge, edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {})
563
+ for flag in ("inferred", "viewer_only")
564
+ )
565
+ and isinstance(preview, dict)
566
+ ):
567
+ agent_path = preview.get("agent_path")
568
+ entry["model_response_preview"] = {
569
+ "schema_version": "1",
570
+ "source_id": source_id,
571
+ "agent_path": agent_path,
572
+ "messages": [dict(message) for message in preview.get("messages", [])
573
+ if isinstance(message, dict) and agent_path
574
+ and message.get("sender") == agent_path],
575
+ "messages_truncated": bool(preview.get("messages_truncated")),
576
+ }
545
577
  entries.append(entry)
546
578
  if isinstance(derived, list):
547
579
  entries.extend(_derived_agent_entries(entry, derived))
@@ -810,14 +842,19 @@ def _mark_shared_evidence(entries: list[dict[str, Any]]) -> None:
810
842
  def conversation_index_payload(
811
843
  graph: dict[str, Any],
812
844
  run_root: str | Path,
845
+ *,
846
+ investigation_cache=None,
847
+ include_investigation: bool = False,
813
848
  ) -> dict[str, Any]:
814
- """Build the conversation index exactly as ``conversations.json`` carries it.
849
+ """Build the shared conversation projection, with opt-in investigation.
815
850
 
816
- The live dashboard and the finalized viewer both read this. Keeping one
817
- builder is the point: a live run that computed its own projection would be
818
- free to disagree with the file written at finalization, and the disagreement
819
- would show up as an agent seeing conversations that are not its own.
851
+ The default is the historical, lightweight conversation contract used by
852
+ ordinary Live polling. Event-stream inspection is requested explicitly by
853
+ the investigation reader and offline exporters. Both modes publish exactly
854
+ the same conversation entries; the extended mode only adds an index.
820
855
  """
856
+ from .investigation_index import build_investigation_index
857
+
821
858
  root = Path(run_root).expanduser().resolve()
822
859
  entries = conversation_record_entries(graph, root)
823
860
  _mark_shared_injected_context(entries)
@@ -830,6 +867,10 @@ def conversation_index_payload(
830
867
  return {
831
868
  "schema_version": "0.3",
832
869
  "scope": "run_local_provider_neutral_conversation_projection",
870
+ "session_id": graph.get("session_id") if isinstance(graph.get("session_id"), str) else None,
871
+ "source_path": graph.get("source_path") if isinstance(graph.get("source_path"), str) else None,
872
+ **({"investigation": build_investigation_index(graph, root, cache=investigation_cache)}
873
+ if include_investigation else {}),
833
874
  "entry_count": len(entries),
834
875
  "visible_message_count": visible_message_count,
835
876
  "external_provider_folder_lookup_required": False,
@@ -852,7 +893,7 @@ def write_conversation_records(
852
893
  root = Path(run_root).expanduser().resolve()
853
894
  root.mkdir(parents=True, exist_ok=True)
854
895
  if payload is None:
855
- payload = conversation_index_payload(graph, root)
896
+ payload = conversation_index_payload(graph, root, include_investigation=True)
856
897
  entries = payload["entries"]
857
898
  json_path = root / "conversations.json"
858
899
  markdown_path = root / "conversations.md"
@@ -4,7 +4,13 @@ import json
4
4
  from typing import Any
5
5
 
6
6
  from .live_view import LIVE_HTML as _BASE_LIVE_HTML
7
+ from .run_assessment import build_run_assessment
8
+ from .viewer_run_assessment import inject_run_assessment
9
+ from .viewer_investigation import inject_investigation
10
+ from .viewer_runtime_evidence import inject_runtime_evidence
7
11
  from .viewer_agent_panel import inject_agent_panel
12
+ from .viewer_framework_model_output import inject_framework_model_output
13
+ from .viewer_content_browser import inject_content_browser
8
14
  from .viewer_dashboard_clean import fold_budget_bootstrap, inject_live_dashboard_clean
9
15
  from .viewer_dashboard_focus import inject_live_dashboard_focus
10
16
  from .viewer_dashboard_pr70 import inject_pr70_dashboard_repairs
@@ -84,7 +90,9 @@ def _build_dashboard_html() -> str:
84
90
  html = normalize_provider_execution_flow(
85
91
  harden_execution_flow_projection(inject_execution_flow(html))
86
92
  )
87
- return _align_agent_panel_topology(inject_agent_panel(_guard_compact_live_snapshot(html)))
93
+ html = _align_agent_panel_topology(inject_agent_panel(_guard_compact_live_snapshot(html)))
94
+ html = inject_framework_model_output(html)
95
+ return inject_runtime_evidence(inject_investigation(inject_run_assessment(inject_content_browser(html))))
88
96
 
89
97
 
90
98
  DASHBOARD_HTML = _build_dashboard_html()
@@ -103,14 +111,21 @@ def render_static_dashboard_html(
103
111
  graph: dict[str, Any],
104
112
  *,
105
113
  conversation_entries: list[dict[str, Any]] | None = None,
114
+ investigation_index: dict[str, Any] | None = None,
106
115
  ) -> str:
107
116
  """Render the exact dashboard shell used by live, backed by embedded snapshots."""
117
+ # Derived presentation metadata belongs beside the raw graph, not in it.
118
+ assessment = graph.get("run_assessment")
119
+ if not isinstance(assessment, dict):
120
+ assessment = build_run_assessment(graph)
108
121
  bootstrap = (
109
122
  "<script>window.__execweaveStaticMode=true;"
110
123
  f"{fold_budget_bootstrap()}"
111
124
  f"{viewer_limits_bootstrap(resolve_viewer_limits())}"
125
+ f"window.__execweaveStaticRunAssessment={_safe_json(assessment)};"
112
126
  f"window.__execweaveStaticGraph={_safe_json(graph)};"
113
127
  f"window.__execweaveStaticConversations={_safe_json(conversation_entries or [])};"
128
+ f"window.__execweaveStaticInvestigation={_safe_json(investigation_index)};"
114
129
  "</script>\n"
115
130
  )
116
131
  html = DASHBOARD_HTML.replace("<script>", bootstrap + "<script>", 1)
@@ -354,7 +354,7 @@ class _DerivedThreads:
354
354
  return
355
355
  self._messages.setdefault(str(agent_path), []).append(message)
356
356
 
357
- def previews(self) -> list[dict[str, Any]]:
357
+ def previews(self, *, preserve_history: bool = False) -> list[dict[str, Any]]:
358
358
  results: list[dict[str, Any]] = []
359
359
  for agent_path in sorted(self._messages):
360
360
  messages = sorted(
@@ -364,7 +364,7 @@ class _DerivedThreads:
364
364
  str(message.get("timestamp") or ""),
365
365
  ),
366
366
  )
367
- truncated = len(messages) > _MAX_PREVIEW_MESSAGES
367
+ truncated = not preserve_history and len(messages) > _MAX_PREVIEW_MESSAGES
368
368
  if truncated:
369
369
  messages = messages[:10] + messages[-(_MAX_PREVIEW_MESSAGES - 10) :]
370
370
  thread_id = self._thread_ids.get(agent_path)
@@ -393,9 +393,15 @@ def codex_rollout_preview(path: str | Path) -> dict[str, Any] | None:
393
393
  return previews[0] if previews else None
394
394
 
395
395
 
396
- def codex_rollout_previews(path: str | Path) -> list[dict[str, Any]]:
396
+ def codex_rollout_previews(
397
+ path: str | Path, *, preserve_history: bool = False,
398
+ ) -> list[dict[str, Any]]:
397
399
  """Extract agent-local conversations a single Codex rollout provides evidence for.
398
400
 
401
+ ``preserve_history`` retains all normalized message records for the Dashboard;
402
+ the default remains the bounded preview used by existing callers. This does
403
+ not change per-message text limits or the provider visibility boundary.
404
+
399
405
  The first result is the rollout's own thread. Codex subagent rollouts can
400
406
  physically contain inherited parent history: newer paginated rollouts persist
401
407
  ordinals on each line, while older compatible rollouts derive them from physical
@@ -597,7 +603,7 @@ def codex_rollout_previews(path: str | Path) -> list[dict[str, Any]]:
597
603
  )
598
604
  except (OSError, RuntimeError, UnicodeError):
599
605
  return []
600
- truncated = len(messages) > _MAX_PREVIEW_MESSAGES
606
+ truncated = not preserve_history and len(messages) > _MAX_PREVIEW_MESSAGES
601
607
  if truncated:
602
608
  messages = messages[:10] + messages[-(_MAX_PREVIEW_MESSAGES - 10) :]
603
609
  owner = {
@@ -612,4 +618,4 @@ def codex_rollout_previews(path: str | Path) -> list[dict[str, Any]]:
612
618
  "messages_truncated": truncated,
613
619
  "messages": messages,
614
620
  }
615
- return [owner, *derived.previews()]
621
+ return [owner, *derived.previews(preserve_history=preserve_history)]
@@ -0,0 +1,289 @@
1
+ """Verify declared archive references without inspecting provider payload bodies.
2
+
3
+ The scope is the exported graph's observed_content nodes and conversation-index
4
+ entries, not provider recall or an adversary-resistant log. Payload JSON is a leaf:
5
+ strings/dictionaries supplied by agents never become additional filesystem paths.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import re
13
+ import stat
14
+ from contextlib import contextmanager
15
+ from pathlib import Path
16
+ from typing import Any, Iterator
17
+
18
+ CONTENT_PATH = re.compile(r"content/sha256/([0-9a-f]{64})\.(?:json|txt|bin)")
19
+ INDEXES = {"graph.json": "nodes", "conversations.json": "entries"}
20
+ EXPORTS = (*INDEXES, "viewer.html")
21
+ CHUNK = 1024 * 1024
22
+
23
+
24
+ class ArchiveReadError(Exception):
25
+ """A bounded, content-free archive diagnostic."""
26
+
27
+
28
+ def _signature(value: os.stat_result) -> tuple[int, ...]:
29
+ return (value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns)
30
+
31
+
32
+ def _reject_link(path: Path) -> None:
33
+ info = path.lstat()
34
+ if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
35
+ raise ArchiveReadError("unsafe_path") # Includes Windows reparse points.
36
+
37
+
38
+ def _path_descriptor_stat(path: Path) -> os.stat_result:
39
+ """Compare two open-file snapshots, not Windows stat/fstat timestamps.
40
+
41
+ Path-based stat and descriptor-based fstat need not expose the same ctime
42
+ semantics on supported Windows/Python versions. Reopening the recorded path
43
+ preserves the identity/size/time comparison without rounding timestamps or
44
+ ignoring changes. This handle is metadata-only and never reads payload bytes.
45
+ """
46
+ _reject_link(path)
47
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
48
+ flags |= getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
49
+ descriptor = os.open(path, flags)
50
+ try:
51
+ value = os.fstat(descriptor)
52
+ if not stat.S_ISREG(value.st_mode):
53
+ raise ArchiveReadError("not_regular_file")
54
+ return value
55
+ finally:
56
+ os.close(descriptor)
57
+
58
+
59
+ @contextmanager
60
+ def _open_regular(root: Path, relative: str) -> Iterator[Any]:
61
+ if relative not in EXPORTS and CONTENT_PATH.fullmatch(relative) is None:
62
+ raise ArchiveReadError("invalid_reference")
63
+ opened: list[int] = []
64
+ try:
65
+ parts = relative.split("/")
66
+ # Pin every directory on POSIX. O_NOFOLLOW prevents symlink traversal even
67
+ # when a directory entry is replaced between validation and opening.
68
+ if os.open in os.supports_dir_fd and hasattr(os, "O_NOFOLLOW"):
69
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
70
+ parent = os.open(root, flags)
71
+ opened.append(parent)
72
+ for part in parts[:-1]:
73
+ parent = os.open(part, flags, dir_fd=parent)
74
+ opened.append(parent)
75
+ fd = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
76
+ else:
77
+ path = root
78
+ _reject_link(path)
79
+ for part in parts:
80
+ path = path / part
81
+ _reject_link(path)
82
+ # Windows has no Python dir_fd/O_NOFOLLOW equivalent. Reject reparse
83
+ # points before and after reading; do not claim tamper-proof storage.
84
+ fd = os.open(path, os.O_RDONLY | getattr(os, "O_BINARY", 0))
85
+ with os.fdopen(fd, "rb") as handle:
86
+ before = os.fstat(handle.fileno())
87
+ if not stat.S_ISREG(before.st_mode):
88
+ raise ArchiveReadError("not_regular_file")
89
+ yield handle
90
+ after = os.fstat(handle.fileno())
91
+ path = root
92
+ _reject_link(path)
93
+ for part in parts:
94
+ path = path / part
95
+ _reject_link(path)
96
+ if _signature(before) != _signature(after) or _signature(after) != _signature(_path_descriptor_stat(path)):
97
+ raise ArchiveReadError("changed_during_read")
98
+ except FileNotFoundError as exc:
99
+ raise ArchiveReadError("missing_file") from exc
100
+ except OSError as exc:
101
+ # Do not write raw exception messages: paths may contain private data.
102
+ raise ArchiveReadError("unreadable_or_unsafe_path") from exc
103
+ finally:
104
+ for fd in reversed(opened):
105
+ os.close(fd)
106
+
107
+
108
+ def _read(root: Path, relative: str, limit: int, *, collect: bool = False) -> tuple[dict[str, Any], bytes]:
109
+ chunks: list[bytes] = []
110
+ digest = hashlib.sha256()
111
+ size = 0
112
+ with _open_regular(root, relative) as handle:
113
+ if os.fstat(handle.fileno()).st_size > limit:
114
+ raise ArchiveReadError("verification_limit")
115
+ while True:
116
+ chunk = handle.read(min(CHUNK, max(1, limit - size + 1)))
117
+ if not chunk:
118
+ break
119
+ size += len(chunk)
120
+ if size > limit:
121
+ raise ArchiveReadError("verification_limit")
122
+ digest.update(chunk)
123
+ if collect:
124
+ chunks.append(chunk)
125
+ return {"sha256": digest.hexdigest(), "size_bytes": size}, b"".join(chunks)
126
+
127
+
128
+ def hash_export(root: Path, name: str) -> dict[str, Any]:
129
+ """Hash only a named, regular primary export, with bounded reads."""
130
+ if name not in EXPORTS:
131
+ raise ArchiveReadError("invalid_export")
132
+ return _read(root, name, 1024 * 1024 * 1024)[0]
133
+
134
+
135
+ def _unique_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
136
+ result: dict[str, Any] = {}
137
+ for key, value in pairs:
138
+ if key in result:
139
+ raise ValueError("duplicate JSON key")
140
+ result[key] = value
141
+ return result
142
+
143
+
144
+ def _constant(value: str) -> None:
145
+ raise ValueError("non-finite JSON value")
146
+
147
+
148
+ def audit_content_references(
149
+ run_root: str | Path,
150
+ *,
151
+ max_index_bytes: int = 64 * 1024 * 1024,
152
+ max_content_bytes: int = 1024 * 1024 * 1024,
153
+ max_references: int = 100_000,
154
+ ) -> dict[str, Any]:
155
+ """Verify registered references; limits and unknown sources never pass silently.
156
+
157
+ Missing size metadata on legacy references is accepted but not invented. Source
158
+ completeness is orthogonal: an intact blob of partial provider content can pass
159
+ byte-integrity verification without implying complete provider observation.
160
+ """
161
+ for limit in (max_index_bytes, max_content_bytes, max_references):
162
+ if type(limit) is not int or limit < 0:
163
+ raise ValueError("limits must be nonnegative integers")
164
+ root = Path(run_root).expanduser().absolute()
165
+ report: dict[str, Any] = {
166
+ "schema_version": "0.1",
167
+ "scope": "declared_graph_and_conversation_content",
168
+ "state": "incomplete",
169
+ "reference_count": 0,
170
+ "reference_count_truncated": False,
171
+ "unique_file_count": 0,
172
+ "verified_file_count": 0,
173
+ "error_count": 0,
174
+ "errors": [],
175
+ "errors_truncated": False,
176
+ "index_files": {},
177
+ "verified_files": {},
178
+ "limits": {"index_bytes": max_index_bytes, "content_bytes": max_content_bytes,
179
+ "references": max_references},
180
+ }
181
+
182
+ def fail(code: str, location: str) -> None:
183
+ report["error_count"] += 1
184
+ if len(report["errors"]) < 100:
185
+ report["errors"].append({"code": code, "location": location})
186
+ else:
187
+ report["errors_truncated"] = True
188
+
189
+ # None is an unknown legacy size, not zero. A duplicate declaration must agree
190
+ # with every known size; a later valid reference cannot erase a conflict.
191
+ references: dict[str, tuple[str, int | None, str]] = {}
192
+ conflicted: set[str] = set()
193
+ limit_hit = False
194
+
195
+ def register(value: Any, location: str) -> None:
196
+ nonlocal limit_hit
197
+ if limit_hit:
198
+ return
199
+ report["reference_count"] += 1
200
+ if report["reference_count"] > max_references:
201
+ limit_hit = True
202
+ report["reference_count_truncated"] = True
203
+ fail("reference_limit", location)
204
+ return
205
+ if not isinstance(value, dict):
206
+ fail("invalid_reference", location)
207
+ return
208
+ path, digest, size = value.get("path"), value.get("sha256"), value.get("size_bytes")
209
+ match = CONTENT_PATH.fullmatch(path) if isinstance(path, str) else None
210
+ if (match is None or match[1] != digest or
211
+ ("size_bytes" in value and (type(size) is not int or size < 0))):
212
+ fail("invalid_reference", location)
213
+ return
214
+ previous = references.get(path)
215
+ if previous and previous[1] is not None and size is not None and previous[1] != size:
216
+ conflicted.add(path)
217
+ fail("conflicting_reference", location)
218
+ return
219
+ if previous is None or previous[1] is None:
220
+ references[path] = (digest, size, location)
221
+
222
+ sessions: set[str] = set()
223
+ for name, key in INDEXES.items():
224
+ try:
225
+ fingerprint, raw = _read(root, name, max_index_bytes, collect=True)
226
+ report["index_files"][name] = fingerprint
227
+ data = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_keys, parse_constant=_constant)
228
+ if not isinstance(data, dict) or not isinstance(data.get(key), list):
229
+ raise ValueError("invalid index shape")
230
+ except ArchiveReadError as exc:
231
+ fail(str(exc), name)
232
+ continue
233
+ except (ValueError, RecursionError):
234
+ fail("invalid_index", name)
235
+ continue
236
+ session = data.get("session_id")
237
+ if isinstance(session, str) and session:
238
+ sessions.add(session)
239
+ for index, item in enumerate(data[key]):
240
+ location = f"{name}/{key}/{index}"
241
+ if not isinstance(item, dict):
242
+ fail("invalid_index_entry", location)
243
+ continue
244
+ if key == "nodes":
245
+ if item.get("type") == "observed_content":
246
+ register(item.get("attributes"), location)
247
+ else:
248
+ # Conversation entries are reference records. Do not walk preview
249
+ # messages, request objects, or arbitrary nested provider JSON.
250
+ if "path" in item or "sha256" in item:
251
+ register(item, location)
252
+ else:
253
+ fail("invalid_reference", location)
254
+ # A projected graph may retain hidden raw nodes under expansion.clusters.
255
+ if key == "nodes" and isinstance(data.get("expansion"), dict):
256
+ clusters = data["expansion"].get("clusters", {})
257
+ if not isinstance(clusters, dict):
258
+ fail("invalid_expansion", f"{name}/expansion/clusters")
259
+ continue
260
+ for cluster_index, cluster in enumerate(clusters.values()):
261
+ location = f"{name}/expansion/clusters/{cluster_index}"
262
+ if not isinstance(cluster, dict) or not isinstance(cluster.get("nodes"), list):
263
+ fail("invalid_expansion", location)
264
+ continue
265
+ for index, item in enumerate(cluster["nodes"]):
266
+ if isinstance(item, dict) and item.get("type") == "observed_content":
267
+ register(item.get("attributes"), f"{location}/nodes/{index}")
268
+ if len(sessions) > 1:
269
+ fail("index_session_mismatch", "graph.json/conversations.json")
270
+ report["unique_file_count"] = len(references)
271
+ budget = max_content_bytes
272
+ for path, (digest, size, location) in references.items():
273
+ if path in conflicted:
274
+ continue
275
+ try:
276
+ fingerprint, _ = _read(root, path, budget)
277
+ budget -= fingerprint["size_bytes"]
278
+ if fingerprint["sha256"] != digest:
279
+ raise ArchiveReadError("hash_mismatch")
280
+ if size is not None and fingerprint["size_bytes"] != size:
281
+ raise ArchiveReadError("size_mismatch")
282
+ report["verified_files"][path] = fingerprint
283
+ except ArchiveReadError as exc:
284
+ fail(str(exc), location)
285
+ report["verified_file_count"] = len(report["verified_files"])
286
+ report["completed_read_bytes"] = max_content_bytes - budget
287
+ if report["error_count"] == 0:
288
+ report["state"] = "complete"
289
+ return report