4141import re
4242import shlex
4343import subprocess
44+ import time
4445from typing import Literal
4546
4647from ouroboros .plugin .digest import (
@@ -1107,11 +1108,14 @@ def _run_failed_invocation_observability_hooks() -> None:
11071108 ).hexdigest ()
11081109 redacted_tool_argv , _ = _redact_argv ([command_name ] + list (argv ))
11091110 tool_args_preview = shlex .join (redacted_tool_argv )
1110- tool_args_digest = hashlib .sha256 (
1111- json .dumps ([command_name ] + list (argv ), separators = ("," , ":" )).encode (
1112- "utf-8" , errors = "surrogateescape"
1113- )
1114- ).hexdigest ()
1111+ tool_args_digest = (
1112+ "sha256:"
1113+ + hashlib .sha256 (
1114+ json .dumps ([command_name ] + list (argv ), separators = ("," , ":" )).encode (
1115+ "utf-8" , errors = "surrogateescape"
1116+ )
1117+ ).hexdigest ()
1118+ )
11151119 tool_name = f"{ namespace } .{ command_name } " if namespace else command_name
11161120 command_permissions = tuple (getattr (command , "permissions" , ()) or ())
11171121 tool_permissions = command_permissions or tuple (_required_permissions (manifest ))
@@ -1173,6 +1177,37 @@ def _run_failed_invocation_observability_hooks() -> None:
11731177 if plugin_home is not None :
11741178 run_kwargs ["cwd" ] = str (plugin_home )
11751179
1180+ tool_call_started_at = time .perf_counter ()
1181+
1182+ def _tool_call_duration_ms () -> int :
1183+ return max (1 , int (round ((time .perf_counter () - tool_call_started_at ) * 1000 )))
1184+
1185+ def _output_digest (stdout_bytes : bytes , stderr_bytes : bytes ) -> str :
1186+ return "sha256:" + hashlib .sha256 (stdout_bytes + stderr_bytes ).hexdigest ()
1187+
1188+ def _dispatch_failed_after_tool_call (
1189+ * ,
1190+ output_digest : str ,
1191+ duration_ms : int ,
1192+ exit_code : int | None ,
1193+ ) -> None :
1194+ dispatch_after_tool_call (
1195+ manifest = manifest ,
1196+ tool = tool_name ,
1197+ status = "failed" ,
1198+ output_digest = output_digest ,
1199+ duration_ms = duration_ms ,
1200+ correlation_id = correlation_id ,
1201+ invocation_id = tool_call_invocation_id ,
1202+ event_sink = _emit ,
1203+ exit_code = exit_code ,
1204+ namespace = namespace ,
1205+ command_name = command_name ,
1206+ trust_state = trust_state ,
1207+ plugin_home = plugin_home ,
1208+ subprocess_runner = runner ,
1209+ )
1210+
11761211 try :
11771212 completed = runner (cmd_argv , ** run_kwargs )
11781213 except FileNotFoundError as exc :
@@ -1191,6 +1226,11 @@ def _run_failed_invocation_observability_hooks() -> None:
11911226 provenance = {"correlation_id" : correlation_id },
11921227 )
11931228 )
1229+ _dispatch_failed_after_tool_call (
1230+ output_digest = _output_digest (b"" , message .encode ("utf-8" , errors = "surrogateescape" )),
1231+ duration_ms = _tool_call_duration_ms (),
1232+ exit_code = 127 ,
1233+ )
11941234 _run_failed_invocation_observability_hooks ()
11951235 return InvocationResult (
11961236 status = "failed" ,
@@ -1208,6 +1248,7 @@ def _run_failed_invocation_observability_hooks() -> None:
12081248 stderr_bytes = _to_bytes (exc .stderr )
12091249 stdout_hash = hashlib .sha256 (stdout_bytes ).hexdigest ()
12101250 stderr_hash = hashlib .sha256 (stderr_bytes ).hexdigest ()
1251+ output_digest = _output_digest (stdout_bytes , stderr_bytes )
12111252 message = (
12121253 f"entrypoint timed out after "
12131254 f"{ DEFAULT_PLUGIN_INVOCATION_TIMEOUT_SECONDS :g} s: { cmd_argv [0 ]!r} "
@@ -1231,6 +1272,11 @@ def _run_failed_invocation_observability_hooks() -> None:
12311272 },
12321273 )
12331274 )
1275+ _dispatch_failed_after_tool_call (
1276+ output_digest = output_digest ,
1277+ duration_ms = _tool_call_duration_ms (),
1278+ exit_code = 124 ,
1279+ )
12341280 _run_failed_invocation_observability_hooks ()
12351281 return InvocationResult (
12361282 status = "failed" ,
@@ -1266,6 +1312,11 @@ def _run_failed_invocation_observability_hooks() -> None:
12661312 },
12671313 )
12681314 )
1315+ _dispatch_failed_after_tool_call (
1316+ output_digest = _output_digest (b"" , message .encode ("utf-8" , errors = "surrogateescape" )),
1317+ duration_ms = _tool_call_duration_ms (),
1318+ exit_code = 126 ,
1319+ )
12691320 _run_failed_invocation_observability_hooks ()
12701321 return InvocationResult (
12711322 status = "failed" ,
@@ -1278,7 +1329,8 @@ def _run_failed_invocation_observability_hooks() -> None:
12781329 stderr_bytes = _to_bytes (completed .stderr )
12791330 stdout_hash = hashlib .sha256 (stdout_bytes ).hexdigest ()
12801331 stderr_hash = hashlib .sha256 (stderr_bytes ).hexdigest ()
1281- output_digest = hashlib .sha256 (stdout_bytes + stderr_bytes ).hexdigest ()
1332+ output_digest = _output_digest (stdout_bytes , stderr_bytes )
1333+ duration_ms = _tool_call_duration_ms ()
12821334
12831335 terminal_provenance = {
12841336 "correlation_id" : correlation_id ,
@@ -1307,7 +1359,7 @@ def _run_failed_invocation_observability_hooks() -> None:
13071359 tool = tool_name ,
13081360 status = "success" ,
13091361 output_digest = output_digest ,
1310- duration_ms = 0 ,
1362+ duration_ms = duration_ms ,
13111363 correlation_id = correlation_id ,
13121364 invocation_id = tool_call_invocation_id ,
13131365 event_sink = _emit ,
@@ -1347,7 +1399,7 @@ def _run_failed_invocation_observability_hooks() -> None:
13471399 tool = tool_name ,
13481400 status = "failed" ,
13491401 output_digest = output_digest ,
1350- duration_ms = 0 ,
1402+ duration_ms = duration_ms ,
13511403 correlation_id = correlation_id ,
13521404 invocation_id = tool_call_invocation_id ,
13531405 event_sink = _emit ,
0 commit comments